From 86f8bf3c2fd9e0dbe0991da35459c92b6b8f06ed Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 8 Mar 2017 21:07:49 +0100 Subject: [PATCH 01/15] Fix some visual bugs --- .../res/drawable/account_header_missing.xml | 3 +- app/src/main/res/drawable/ic_back.xml | 7 ----- app/src/main/res/drawable/ic_compose.xml | 11 ------- app/src/main/res/drawable/ic_extra.xml | 7 ----- app/src/main/res/drawable/ic_favourite.xml | 7 ----- app/src/main/res/drawable/ic_favourited.xml | 7 ----- app/src/main/res/drawable/ic_followed.xml | 7 ----- app/src/main/res/drawable/ic_lock_24dp.xml | 9 ------ app/src/main/res/drawable/ic_media.xml | 31 ------------------- .../main/res/drawable/ic_media_disabled.xml | 11 ------- .../main/res/drawable/ic_notify_mention.xml | 11 ------- app/src/main/res/drawable/ic_options.xml | 7 ----- .../res/drawable/ic_person_outline_24dp.xml | 9 ------ app/src/main/res/drawable/ic_reblog.xml | 7 ----- .../main/res/drawable/ic_reblog_disabled.xml | 7 ----- app/src/main/res/drawable/ic_reblogged.xml | 7 ----- app/src/main/res/drawable/ic_reply.xml | 7 ----- app/src/main/res/layout/item_account.xml | 12 +++++-- app/src/main/res/layout/tab_main.xml | 14 --------- .../main/res/transition/activity_slide.xml | 3 -- 20 files changed, 10 insertions(+), 174 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_back.xml delete mode 100644 app/src/main/res/drawable/ic_compose.xml delete mode 100644 app/src/main/res/drawable/ic_extra.xml delete mode 100644 app/src/main/res/drawable/ic_favourite.xml delete mode 100644 app/src/main/res/drawable/ic_favourited.xml delete mode 100644 app/src/main/res/drawable/ic_followed.xml delete mode 100644 app/src/main/res/drawable/ic_lock_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_media.xml delete mode 100644 app/src/main/res/drawable/ic_media_disabled.xml delete mode 100644 app/src/main/res/drawable/ic_notify_mention.xml delete mode 100644 app/src/main/res/drawable/ic_options.xml delete mode 100644 app/src/main/res/drawable/ic_person_outline_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_reblog.xml delete mode 100644 app/src/main/res/drawable/ic_reblog_disabled.xml delete mode 100644 app/src/main/res/drawable/ic_reblogged.xml delete mode 100644 app/src/main/res/drawable/ic_reply.xml delete mode 100644 app/src/main/res/layout/tab_main.xml delete mode 100644 app/src/main/res/transition/activity_slide.xml diff --git a/app/src/main/res/drawable/account_header_missing.xml b/app/src/main/res/drawable/account_header_missing.xml index 0567c24b..51629946 100644 --- a/app/src/main/res/drawable/account_header_missing.xml +++ b/app/src/main/res/drawable/account_header_missing.xml @@ -2,7 +2,6 @@ - + - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml deleted file mode 100644 index cceab543..00000000 --- a/app/src/main/res/drawable/ic_back.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_compose.xml b/app/src/main/res/drawable/ic_compose.xml deleted file mode 100644 index c5669bb7..00000000 --- a/app/src/main/res/drawable/ic_compose.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_extra.xml b/app/src/main/res/drawable/ic_extra.xml deleted file mode 100644 index 62fddb87..00000000 --- a/app/src/main/res/drawable/ic_extra.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_favourite.xml b/app/src/main/res/drawable/ic_favourite.xml deleted file mode 100644 index 24478b8b..00000000 --- a/app/src/main/res/drawable/ic_favourite.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_favourited.xml b/app/src/main/res/drawable/ic_favourited.xml deleted file mode 100644 index fe780bc2..00000000 --- a/app/src/main/res/drawable/ic_favourited.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_followed.xml b/app/src/main/res/drawable/ic_followed.xml deleted file mode 100644 index bdef7ee8..00000000 --- a/app/src/main/res/drawable/ic_followed.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_lock_24dp.xml b/app/src/main/res/drawable/ic_lock_24dp.xml deleted file mode 100644 index 6316164d..00000000 --- a/app/src/main/res/drawable/ic_lock_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_media.xml b/app/src/main/res/drawable/ic_media.xml deleted file mode 100644 index ee873111..00000000 --- a/app/src/main/res/drawable/ic_media.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_media_disabled.xml b/app/src/main/res/drawable/ic_media_disabled.xml deleted file mode 100644 index 9c1a90e9..00000000 --- a/app/src/main/res/drawable/ic_media_disabled.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_notify_mention.xml b/app/src/main/res/drawable/ic_notify_mention.xml deleted file mode 100644 index 6ee8c1ce..00000000 --- a/app/src/main/res/drawable/ic_notify_mention.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_options.xml b/app/src/main/res/drawable/ic_options.xml deleted file mode 100644 index b4f39bea..00000000 --- a/app/src/main/res/drawable/ic_options.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_person_outline_24dp.xml b/app/src/main/res/drawable/ic_person_outline_24dp.xml deleted file mode 100644 index 4ad4f8d0..00000000 --- a/app/src/main/res/drawable/ic_person_outline_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_reblog.xml b/app/src/main/res/drawable/ic_reblog.xml deleted file mode 100644 index 3666855f..00000000 --- a/app/src/main/res/drawable/ic_reblog.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_reblog_disabled.xml b/app/src/main/res/drawable/ic_reblog_disabled.xml deleted file mode 100644 index afafe08e..00000000 --- a/app/src/main/res/drawable/ic_reblog_disabled.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_reblogged.xml b/app/src/main/res/drawable/ic_reblogged.xml deleted file mode 100644 index 1f9d8b64..00000000 --- a/app/src/main/res/drawable/ic_reblogged.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_reply.xml b/app/src/main/res/drawable/ic_reply.xml deleted file mode 100644 index 3df673b9..00000000 --- a/app/src/main/res/drawable/ic_reply.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml index 7f1f9dc1..44f5739c 100644 --- a/app/src/main/res/layout/item_account.xml +++ b/app/src/main/res/layout/item_account.xml @@ -3,16 +3,18 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="16dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" android:id="@+id/account_container"> @@ -20,6 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" + android:layout_centerVertical="true" android:layout_toRightOf="@id/account_avatar"> @@ -27,12 +30,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/account_display_name" + android:text="Display name" android:textColor="?android:textColorPrimary" android:textStyle="normal|bold" /> @@ -45,6 +50,7 @@ android:layout_height="wrap_content" android:id="@+id/account_note" android:paddingTop="4dp" + android:paddingBottom="8dp" android:textColor="?android:textColorTertiary" /> \ No newline at end of file diff --git a/app/src/main/res/layout/tab_main.xml b/app/src/main/res/layout/tab_main.xml deleted file mode 100644 index 5684fffd..00000000 --- a/app/src/main/res/layout/tab_main.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/transition/activity_slide.xml b/app/src/main/res/transition/activity_slide.xml deleted file mode 100644 index cd343707..00000000 --- a/app/src/main/res/transition/activity_slide.xml +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file From cff0f352699c3c503ae8c8c7e0e401e958e4df76 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 8 Mar 2017 22:08:50 +0100 Subject: [PATCH 02/15] Start work on integrating Retrofit - Mastodon API definition --- app/build.gradle | 1 + .../keylesspalace/tusky/MastodonService.java | 105 ++++++++++++++++++ .../com/keylesspalace/tusky/entity/Media.java | 49 ++++++++ .../tusky/entity/Relationship.java | 58 ++++++++++ .../tusky/entity/StatusContext.java | 27 +++++ 5 files changed, 240 insertions(+) create mode 100644 app/src/main/java/com/keylesspalace/tusky/MastodonService.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Media.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java diff --git a/app/build.gradle b/app/build.gradle index c9c6c400..7c6d43e2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,4 +35,5 @@ dependencies { compile 'com.github.peter9870:sparkbutton:master' testCompile 'junit:junit:4.12' compile 'com.mikhaellopez:circularfillableloaders:1.2.0' + compile 'com.squareup.retrofit2:retrofit:2.2.0' } diff --git a/app/src/main/java/com/keylesspalace/tusky/MastodonService.java b/app/src/main/java/com/keylesspalace/tusky/MastodonService.java new file mode 100644 index 00000000..1a74781f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/MastodonService.java @@ -0,0 +1,105 @@ +package com.keylesspalace.tusky; + +import com.keylesspalace.tusky.entity.Media; +import com.keylesspalace.tusky.entity.Relationship; +import com.keylesspalace.tusky.entity.StatusContext; + +import java.util.List; + +import okhttp3.RequestBody; +import retrofit2.Call; +import retrofit2.http.DELETE; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; +import retrofit2.http.Multipart; +import retrofit2.http.POST; +import retrofit2.http.Part; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface MastodonService { + @GET("api/v1/timelines/home") + Call> homeTimeline(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + @GET("api/v1/timelines/public") + Call> publicTimeline(@Query("local") boolean local, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + @GET("api/v1/timelines/tag/{hashtag}") + Call> hashtagTimeline(@Path("hashtag") String hashtag, @Query("local") boolean local, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + + @GET("api/v1/notifications") + Call> notifications(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + @POST("api/v1/notifications/clear") + Call clearNotifications(); + @GET("api/v1/notifications/{id}") + Call notification(@Path("id") int notificationId); + + @Multipart + @POST("api/v1/media") + Call uploadMedia(@Part("file") RequestBody file); + + @FormUrlEncoded + @POST("api/v1/statuses") + Call createStatus(@Field("status") String text, @Field("in_reply_to_id") int inReplyToId, @Field("spoiler_text") String warningText, @Field("visibility") String visibility, @Field("sensitive") boolean sensitive, @Field("media_ids[]") List mediaIds); + @GET("api/v1/statuses/{id}") + Call status(@Path("id") int statusId); + @GET("api/v1/statuses/{id}/context") + Call statusContext(@Path("id") int statusId); + @GET("api/v1/statuses/{id}/reblogged_by") + Call> statusRebloggedBy(@Path("id") int statusId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + @GET("api/v1/statuses/{id}/favourited_by") + Call> statusFavouritedBy(@Path("id") int statusId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + @DELETE("api/v1/statuses/{id}") + Call deleteStatus(@Path("id") int statusId); + @POST("api/v1/statuses/{id}/reblog") + Call reblogStatus(@Path("id") int statusId); + @POST("api/v1/statuses/{id}/unreblog") + Call unreblogStatus(@Path("id") int statusId); + @POST("api/v1/statuses/{id}/favourite") + Call favouriteStatus(@Path("id") int statusId); + @POST("api/v1/statuses/{id}/unfavourite") + Call unfavouriteStatus(@Path("id") int statusId); + + @GET("api/v1/accounts/verify_credentials") + Call accountVerifyCredentials(); + @GET("api/v1/accounts/search") + Call> searchAccounts(@Query("q") String q, @Query("resolve") boolean resolve, @Query("limit") int limit); + @GET("api/v1/accounts/{id}") + Call account(@Path("id") int accountId); + @GET("api/v1/accounts/{id}/statuses") + Call> accountStatuses(@Path("id") int accountId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + @GET("api/v1/accounts/{id}/followers") + Call> accountFollowers(@Path("id") int accountId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + @GET("api/v1/accounts/{id}/following") + Call> accountFollowing(@Path("id") int accountId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + @POST("api/v1/accounts/{id}/follow") + Call followAccount(@Path("id") int accountId); + @POST("api/v1/accounts/{id}/unfollow") + Call unfollowAccount(@Path("id") int accountId); + @POST("api/v1/accounts/{id}/block") + Call blockAccount(@Path("id") int accountId); + @POST("api/v1/accounts/{id}/unblock") + Call unblockAccount(@Path("id") int accountId); + @POST("api/v1/accounts/{id}/mute") + Call muteAccount(@Path("id") int accountId); + @POST("api/v1/accounts/{id}/unmute") + Call unmuteAccount(@Path("id") int accountId); + + @GET("api/v1/accounts/relationships") + Call> relationships(@Query("id[]") List accountIds); + + @GET("api/v1/blocks") + Call> blocks(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + + @GET("api/v1/mutes") + Call> mutes(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + + @GET("api/v1/favourites") + Call> favourites(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + + @GET("api/v1/follow_requests") + Call> followRequests(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + @POST("api/v1/follow_requests/{id}/authorize") + Call authorizeFollowRequest(@Path("id") int accountId); + @POST("api/v1/follow_requests/{id}/reject") + Call rejectFollowRequest(@Path("id") int accountId); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Media.java b/app/src/main/java/com/keylesspalace/tusky/entity/Media.java new file mode 100644 index 00000000..38c40c09 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Media.java @@ -0,0 +1,49 @@ +package com.keylesspalace.tusky.entity; + +public class Media { + int id; + String type; + String url; + String preview_url; + String text_url; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getPreview_url() { + return preview_url; + } + + public void setPreview_url(String preview_url) { + this.preview_url = preview_url; + } + + public String getText_url() { + return text_url; + } + + public void setText_url(String text_url) { + this.text_url = text_url; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java new file mode 100644 index 00000000..ffffc945 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java @@ -0,0 +1,58 @@ +package com.keylesspalace.tusky.entity; + +public class Relationship { + public boolean isFollowing() { + return following; + } + + public void setFollowing(boolean following) { + this.following = following; + } + + public boolean isFollowed_by() { + return followed_by; + } + + public void setFollowed_by(boolean followed_by) { + this.followed_by = followed_by; + } + + public boolean isBlocking() { + return blocking; + } + + public void setBlocking(boolean blocking) { + this.blocking = blocking; + } + + public boolean isMuting() { + return muting; + } + + public void setMuting(boolean muting) { + this.muting = muting; + } + + public boolean isRequested() { + return requested; + } + + public void setRequested(boolean requested) { + this.requested = requested; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + int id; + boolean following; + boolean followed_by; + boolean blocking; + boolean muting; + boolean requested; +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java new file mode 100644 index 00000000..15d2fded --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java @@ -0,0 +1,27 @@ +package com.keylesspalace.tusky.entity; + +import com.keylesspalace.tusky.Status; + +import java.util.List; + +public class StatusContext { + List ancestors; + + public List getAncestors() { + return ancestors; + } + + public void setAncestors(List ancestors) { + this.ancestors = ancestors; + } + + public List getDescendants() { + return descendants; + } + + public void setDescendants(List descendants) { + this.descendants = descendants; + } + + List descendants; +} From 348d2c8b4f7b3a35de7e6bd53efc47ed106c4848 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 8 Mar 2017 22:34:13 +0100 Subject: [PATCH 03/15] Start work on integrating Retrofit - GSON, Authorization --- app/build.gradle | 1 + .../com/keylesspalace/tusky/BaseActivity.java | 49 ++++++++++++++++ ...{MastodonService.java => MastodonAPI.java} | 7 ++- .../com/keylesspalace/tusky/entity/Media.java | 50 +++------------- .../tusky/entity/Relationship.java | 58 +++---------------- .../tusky/entity/StatusContext.java | 21 +------ 6 files changed, 74 insertions(+), 112 deletions(-) rename app/src/main/java/com/keylesspalace/tusky/{MastodonService.java => MastodonAPI.java} (97%) diff --git a/app/build.gradle b/app/build.gradle index 7c6d43e2..762e622d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,4 +36,5 @@ dependencies { testCompile 'junit:junit:4.12' compile 'com.mikhaellopez:circularfillableloaders:1.2.0' compile 'com.squareup.retrofit2:retrofit:2.2.0' + compile 'com.squareup.retrofit2:converter-gson:2.1.0' } diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index bb3a4fb5..4725c25c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -15,7 +15,9 @@ package com.keylesspalace.tusky; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; @@ -26,11 +28,22 @@ import android.support.v7.app.AppCompatActivity; import android.util.TypedValue; import android.view.Menu; +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + /* There isn't presently a way to globally change the theme of a whole application at runtime, just * individual activities. So, each activity has to set its theme before any views are created. And * the most expedient way to accomplish this was to put it in a base class and just have every * activity extend from it. */ public class BaseActivity extends AppCompatActivity { + protected MastodonAPI mastodonAPI; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -59,6 +72,42 @@ public class BaseActivity extends AppCompatActivity { overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right); } + protected String getAccessToken() { + SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE); + return preferences.getString("accessToken", null); + } + + protected String getBaseUrl() { + SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE); + return "https://" + preferences.getString("domain", null); + } + + protected void createMastodonAPI() { + OkHttpClient okHttpClient = new OkHttpClient.Builder() + .addInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + + Request.Builder builder = originalRequest.newBuilder() + .header("Authorization", String.format("Bearer %s", getAccessToken())); + + Request newRequest = builder.build(); + + return chain.proceed(newRequest); + } + }) + .build(); + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(getBaseUrl()) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + mastodonAPI = retrofit.create(MastodonAPI.class); + } + @Override public boolean onCreateOptionsMenu(Menu menu) { TypedValue value = new TypedValue(); diff --git a/app/src/main/java/com/keylesspalace/tusky/MastodonService.java b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/MastodonService.java rename to app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java index 1a74781f..06c98589 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MastodonService.java +++ b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java @@ -7,6 +7,7 @@ import com.keylesspalace.tusky.entity.StatusContext; import java.util.List; import okhttp3.RequestBody; +import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.DELETE; import retrofit2.http.Field; @@ -18,7 +19,7 @@ import retrofit2.http.Part; import retrofit2.http.Path; import retrofit2.http.Query; -public interface MastodonService { +public interface MastodonAPI { @GET("api/v1/timelines/home") Call> homeTimeline(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); @GET("api/v1/timelines/public") @@ -29,7 +30,7 @@ public interface MastodonService { @GET("api/v1/notifications") Call> notifications(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); @POST("api/v1/notifications/clear") - Call clearNotifications(); + Call clearNotifications(); @GET("api/v1/notifications/{id}") Call notification(@Path("id") int notificationId); @@ -49,7 +50,7 @@ public interface MastodonService { @GET("api/v1/statuses/{id}/favourited_by") Call> statusFavouritedBy(@Path("id") int statusId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); @DELETE("api/v1/statuses/{id}") - Call deleteStatus(@Path("id") int statusId); + Call deleteStatus(@Path("id") int statusId); @POST("api/v1/statuses/{id}/reblog") Call reblogStatus(@Path("id") int statusId); @POST("api/v1/statuses/{id}/unreblog") diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Media.java b/app/src/main/java/com/keylesspalace/tusky/entity/Media.java index 38c40c09..ec6ef31c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Media.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Media.java @@ -1,49 +1,17 @@ package com.keylesspalace.tusky.entity; +import com.google.gson.annotations.SerializedName; + public class Media { - int id; - String type; - String url; - String preview_url; - String text_url; + public int id; - public int getId() { - return id; - } + public String type; - public void setId(int id) { - this.id = id; - } + public String url; - public String getType() { - return type; - } + @SerializedName("preview_url") + public String previewUrl; - public void setType(String type) { - this.type = type; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getPreview_url() { - return preview_url; - } - - public void setPreview_url(String preview_url) { - this.preview_url = preview_url; - } - - public String getText_url() { - return text_url; - } - - public void setText_url(String text_url) { - this.text_url = text_url; - } + @SerializedName("text_url") + public String textUrl; } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java index ffffc945..6964b585 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java @@ -1,58 +1,18 @@ package com.keylesspalace.tusky.entity; +import com.google.gson.annotations.SerializedName; + public class Relationship { - public boolean isFollowing() { - return following; - } + public int id; - public void setFollowing(boolean following) { - this.following = following; - } + public boolean following; - public boolean isFollowed_by() { - return followed_by; - } + @SerializedName("followed_by") + public boolean followedBy; - public void setFollowed_by(boolean followed_by) { - this.followed_by = followed_by; - } + public boolean blocking; - public boolean isBlocking() { - return blocking; - } + public boolean muting; - public void setBlocking(boolean blocking) { - this.blocking = blocking; - } - - public boolean isMuting() { - return muting; - } - - public void setMuting(boolean muting) { - this.muting = muting; - } - - public boolean isRequested() { - return requested; - } - - public void setRequested(boolean requested) { - this.requested = requested; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - int id; - boolean following; - boolean followed_by; - boolean blocking; - boolean muting; - boolean requested; + public boolean requested; } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java index 15d2fded..4099df6b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java @@ -5,23 +5,6 @@ import com.keylesspalace.tusky.Status; import java.util.List; public class StatusContext { - List ancestors; - - public List getAncestors() { - return ancestors; - } - - public void setAncestors(List ancestors) { - this.ancestors = ancestors; - } - - public List getDescendants() { - return descendants; - } - - public void setDescendants(List descendants) { - this.descendants = descendants; - } - - List descendants; + public List ancestors; + public List descendants; } From 3120fbed4cd40ad02cc11f2f9700813ea9709113 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 8 Mar 2017 23:19:03 +0100 Subject: [PATCH 04/15] Start work on integrating Retrofit - Update Account entity to be fetched with Retrofit and parsed with GSON --- .../java/com/keylesspalace/tusky/Account.java | 92 ------------- .../keylesspalace/tusky/AccountActivity.java | 43 ++----- .../keylesspalace/tusky/AccountAdapter.java | 2 + .../keylesspalace/tusky/AccountFragment.java | 58 +++------ .../com/keylesspalace/tusky/BaseActivity.java | 10 +- .../keylesspalace/tusky/BlocksAdapter.java | 1 + .../keylesspalace/tusky/FollowAdapter.java | 1 + .../com/keylesspalace/tusky/MastodonAPI.java | 121 +++++++++++++----- .../tusky/SpannedTypeAdapter.java | 17 +++ .../keylesspalace/tusky/entity/Account.java | 63 +++++++++ .../com/keylesspalace/tusky/entity/Media.java | 2 +- .../tusky/entity/Relationship.java | 2 +- 12 files changed, 219 insertions(+), 193 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/Account.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Account.java diff --git a/app/src/main/java/com/keylesspalace/tusky/Account.java b/app/src/main/java/com/keylesspalace/tusky/Account.java deleted file mode 100644 index ae88d2f3..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/Account.java +++ /dev/null @@ -1,92 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is part of Tusky. - * - * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU - * General Public License as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky. If not, see - * . */ - -package com.keylesspalace.tusky; - -import android.text.Spanned; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; - -class Account { - String id; - String username; - String displayName; - Spanned note; - String url; - String avatar; - String header; - String followersCount; - String followingCount; - String statusesCount; - - public static Account parse(JSONObject object) throws JSONException { - Account account = new Account(); - account.id = object.getString("id"); - account.username = object.getString("acct"); - account.displayName = object.getString("display_name"); - if (account.displayName.isEmpty()) { - account.displayName = object.getString("username"); - } - account.note = HtmlUtils.fromHtml(object.getString("note")); - account.url = object.getString("url"); - String avatarUrl = object.getString("avatar"); - if (!avatarUrl.equals("/avatars/original/missing.png")) { - account.avatar = avatarUrl; - } else { - account.avatar = null; - } - String headerUrl = object.getString("header"); - if (!headerUrl.equals("/headers/original/missing.png")) { - account.header = headerUrl; - } else { - account.header = null; - } - account.followersCount = object.getString("followers_count"); - account.followingCount = object.getString("following_count"); - account.statusesCount = object.getString("statuses_count"); - return account; - } - - public static List parse(JSONArray array) throws JSONException { - List accounts = new ArrayList<>(); - for (int i = 0; i < array.length(); i++) { - JSONObject object = array.getJSONObject(i); - Account account = parse(object); - accounts.add(account); - } - return accounts; - } - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (this.id == null) { - return this == other; - } else if (!(other instanceof Account)) { - return false; - } - Account account = (Account) other; - return account.id.equals(this.id); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index db7d2864..f517d402 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -45,6 +45,7 @@ import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonArrayRequest; import com.android.volley.toolbox.JsonObjectRequest; +import com.keylesspalace.tusky.entity.Account; import com.pkmmte.view.CircularImageView; import com.squareup.picasso.Picasso; @@ -55,6 +56,9 @@ import org.json.JSONObject; import java.util.HashMap; import java.util.Map; +import retrofit2.Call; +import retrofit2.Callback; + public class AccountActivity extends BaseActivity { private static final String TAG = "AccountActivity"; // Volley request tag and logging tag @@ -71,6 +75,7 @@ public class AccountActivity extends BaseActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_account); + createMastodonAPI(); Intent intent = getIntent(); accountId = intent.getStringExtra("id"); @@ -169,37 +174,17 @@ public class AccountActivity extends BaseActivity { } private void obtainAccount() { - String endpoint = String.format(getString(R.string.endpoint_accounts), accountId); - String url = "https://" + domain + endpoint; - JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, null, - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - Account account; - try { - account = Account.parse(response); - } catch (JSONException e) { - onObtainAccountFailure(); - return; - } - onObtainAccountSuccess(account); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onObtainAccountFailure(); - } - }) { + mastodonAPI.account(accountId).enqueue(new Callback() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call call, retrofit2.Response response) { + onObtainAccountSuccess(response.body()); } - }; - request.setTag(TAG); - VolleySingleton.getInstance(this).addToRequestQueue(request); + + @Override + public void onFailure(Call call, Throwable t) { + onObtainAccountFailure(); + } + }); } private void onObtainAccountSuccess(Account account) { diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java b/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java index 6fd60a6f..ab7653a6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java @@ -17,6 +17,8 @@ package com.keylesspalace.tusky; import android.support.v7.widget.RecyclerView; +import com.keylesspalace.tusky.entity.Account; + import java.util.ArrayList; import java.util.List; diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java index c3e7be81..35887516 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java @@ -34,16 +34,16 @@ import com.android.volley.AuthFailureError; import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; -import com.android.volley.toolbox.JsonArrayRequest; import com.android.volley.toolbox.StringRequest; - -import org.json.JSONArray; -import org.json.JSONException; +import com.keylesspalace.tusky.entity.Account; import java.util.HashMap; import java.util.List; import java.util.Map; +import retrofit2.Call; +import retrofit2.Callback; + public class AccountFragment extends Fragment implements AccountActionListener, FooterActionListener { private static final String TAG = "Account"; // logging tag and Volley request tag @@ -170,55 +170,39 @@ public class AccountFragment extends Fragment implements AccountActionListener, } private void fetchAccounts(final String fromId) { + MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI; + + Callback> cb = new Callback>() { + @Override + public void onResponse(Call> call, retrofit2.Response> response) { + onFetchAccountsSuccess(response.body(), fromId); + } + + @Override + public void onFailure(Call> call, Throwable t) { + onFetchAccountsFailure((Exception) t); + } + }; + String endpoint; switch (type) { default: case FOLLOWS: { endpoint = String.format(getString(R.string.endpoint_following), accountId); + api.accountFollowing(accountId, fromId, null, null).enqueue(cb); break; } case FOLLOWERS: { endpoint = String.format(getString(R.string.endpoint_followers), accountId); + api.accountFollowers(accountId, fromId, null, null).enqueue(cb); break; } case BLOCKS: { endpoint = getString(R.string.endpoint_blocks); + api.blocks(fromId, null, null).enqueue(cb); break; } } - String url = "https://" + domain + endpoint; - if (fromId != null) { - url += "?max_id=" + fromId; - } - JsonArrayRequest request = new JsonArrayRequest(url, - new Response.Listener() { - @Override - public void onResponse(JSONArray response) { - List accounts; - try { - accounts = Account.parse(response); - } catch (JSONException e) { - onFetchAccountsFailure(e); - return; - } - onFetchAccountsSuccess(accounts, fromId); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onFetchAccountsFailure(error); - } - }) { - @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; - } - }; - request.setTag(TAG); - VolleySingleton.getInstance(getContext()).addToRequestQueue(request); } private void fetchAccounts() { diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index 4725c25c..7568dccd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -25,9 +25,13 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; +import android.text.Spanned; import android.util.TypedValue; import android.view.Menu; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + import java.io.IOException; import okhttp3.Interceptor; @@ -99,10 +103,14 @@ public class BaseActivity extends AppCompatActivity { }) .build(); + Gson gson = new GsonBuilder() + .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) + .create(); + Retrofit retrofit = new Retrofit.Builder() .baseUrl(getBaseUrl()) .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) .build(); mastodonAPI = retrofit.create(MastodonAPI.class); diff --git a/app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java index 0b440547..37bfc403 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java @@ -23,6 +23,7 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import com.keylesspalace.tusky.entity.Account; import com.squareup.picasso.Picasso; import java.util.HashSet; diff --git a/app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java b/app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java index de70bcee..140e32d0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java @@ -23,6 +23,7 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import com.keylesspalace.tusky.entity.Account; import com.squareup.picasso.Picasso; /** Both for follows and following lists. */ diff --git a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java index 06c98589..846ad3e3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java +++ b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java @@ -1,5 +1,6 @@ package com.keylesspalace.tusky; +import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Media; import com.keylesspalace.tusky.entity.Relationship; import com.keylesspalace.tusky.entity.StatusContext; @@ -21,18 +22,33 @@ import retrofit2.http.Query; public interface MastodonAPI { @GET("api/v1/timelines/home") - Call> homeTimeline(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> homeTimeline( + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/timelines/public") - Call> publicTimeline(@Query("local") boolean local, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> publicTimeline( + @Query("local") Boolean local, + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/timelines/tag/{hashtag}") - Call> hashtagTimeline(@Path("hashtag") String hashtag, @Query("local") boolean local, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> hashtagTimeline( + @Path("hashtag") String hashtag, + @Query("local") Boolean local, + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/notifications") - Call> notifications(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> notifications( + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @POST("api/v1/notifications/clear") Call clearNotifications(); @GET("api/v1/notifications/{id}") - Call notification(@Path("id") int notificationId); + Call notification(@Path("id") String notificationId); @Multipart @POST("api/v1/media") @@ -40,67 +56,108 @@ public interface MastodonAPI { @FormUrlEncoded @POST("api/v1/statuses") - Call createStatus(@Field("status") String text, @Field("in_reply_to_id") int inReplyToId, @Field("spoiler_text") String warningText, @Field("visibility") String visibility, @Field("sensitive") boolean sensitive, @Field("media_ids[]") List mediaIds); + Call createStatus( + @Field("status") String text, + @Field("in_reply_to_id") String inReplyToId, + @Field("spoiler_text") String warningText, + @Field("visibility") String visibility, + @Field("sensitive") Boolean sensitive, + @Field("media_ids[]") List mediaIds); @GET("api/v1/statuses/{id}") - Call status(@Path("id") int statusId); + Call status(@Path("id") String statusId); @GET("api/v1/statuses/{id}/context") - Call statusContext(@Path("id") int statusId); + Call statusContext(@Path("id") String statusId); @GET("api/v1/statuses/{id}/reblogged_by") - Call> statusRebloggedBy(@Path("id") int statusId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> statusRebloggedBy( + @Path("id") String statusId, + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/statuses/{id}/favourited_by") - Call> statusFavouritedBy(@Path("id") int statusId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> statusFavouritedBy( + @Path("id") String statusId, + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @DELETE("api/v1/statuses/{id}") - Call deleteStatus(@Path("id") int statusId); + Call deleteStatus(@Path("id") String statusId); @POST("api/v1/statuses/{id}/reblog") - Call reblogStatus(@Path("id") int statusId); + Call reblogStatus(@Path("id") String statusId); @POST("api/v1/statuses/{id}/unreblog") - Call unreblogStatus(@Path("id") int statusId); + Call unreblogStatus(@Path("id") String statusId); @POST("api/v1/statuses/{id}/favourite") - Call favouriteStatus(@Path("id") int statusId); + Call favouriteStatus(@Path("id") String statusId); @POST("api/v1/statuses/{id}/unfavourite") - Call unfavouriteStatus(@Path("id") int statusId); + Call unfavouriteStatus(@Path("id") String statusId); @GET("api/v1/accounts/verify_credentials") Call accountVerifyCredentials(); @GET("api/v1/accounts/search") - Call> searchAccounts(@Query("q") String q, @Query("resolve") boolean resolve, @Query("limit") int limit); + Call> searchAccounts( + @Query("q") String q, + @Query("resolve") Boolean resolve, + @Query("limit") Integer limit); @GET("api/v1/accounts/{id}") - Call account(@Path("id") int accountId); + Call account(@Path("id") String accountId); @GET("api/v1/accounts/{id}/statuses") - Call> accountStatuses(@Path("id") int accountId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> accountStatuses( + @Path("id") String accountId, + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/accounts/{id}/followers") - Call> accountFollowers(@Path("id") int accountId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> accountFollowers( + @Path("id") String accountId, + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/accounts/{id}/following") - Call> accountFollowing(@Path("id") int accountId, @Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> accountFollowing( + @Path("id") String accountId, + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @POST("api/v1/accounts/{id}/follow") - Call followAccount(@Path("id") int accountId); + Call followAccount(@Path("id") String accountId); @POST("api/v1/accounts/{id}/unfollow") - Call unfollowAccount(@Path("id") int accountId); + Call unfollowAccount(@Path("id") String accountId); @POST("api/v1/accounts/{id}/block") - Call blockAccount(@Path("id") int accountId); + Call blockAccount(@Path("id") String accountId); @POST("api/v1/accounts/{id}/unblock") - Call unblockAccount(@Path("id") int accountId); + Call unblockAccount(@Path("id") String accountId); @POST("api/v1/accounts/{id}/mute") - Call muteAccount(@Path("id") int accountId); + Call muteAccount(@Path("id") String accountId); @POST("api/v1/accounts/{id}/unmute") - Call unmuteAccount(@Path("id") int accountId); + Call unmuteAccount(@Path("id") String accountId); @GET("api/v1/accounts/relationships") Call> relationships(@Query("id[]") List accountIds); @GET("api/v1/blocks") - Call> blocks(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> blocks( + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/mutes") - Call> mutes(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> mutes( + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/favourites") - Call> favourites(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> favourites( + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/follow_requests") - Call> followRequests(@Query("max_id") int maxId, @Query("since_id") int sinceId, @Query("limit") int limit); + Call> followRequests( + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @POST("api/v1/follow_requests/{id}/authorize") - Call authorizeFollowRequest(@Path("id") int accountId); + Call authorizeFollowRequest(@Path("id") String accountId); @POST("api/v1/follow_requests/{id}/reject") - Call rejectFollowRequest(@Path("id") int accountId); + Call rejectFollowRequest(@Path("id") String accountId); } diff --git a/app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java b/app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java new file mode 100644 index 00000000..1ef4db1c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java @@ -0,0 +1,17 @@ +package com.keylesspalace.tusky; + +import android.text.Spanned; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; + +class SpannedTypeAdapter implements JsonDeserializer { + @Override + public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return HtmlUtils.fromHtml(json.getAsString()); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.java b/app/src/main/java/com/keylesspalace/tusky/entity/Account.java new file mode 100644 index 00000000..6f03b030 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.java @@ -0,0 +1,63 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky.entity; + +import android.text.Spanned; + +import com.google.gson.annotations.SerializedName; + +public class Account { + public String id; + + @SerializedName("acct") + public String username; + + @SerializedName("display_name") + public String displayName; + + public Spanned note; + + public String url; + + public String avatar; + + public String header; + + @SerializedName("followers_count") + public String followersCount; + + @SerializedName("following_count") + public String followingCount; + + @SerializedName("statuses_count") + public String statusesCount; + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (this.id == null) { + return this == other; + } else if (!(other instanceof Account)) { + return false; + } + Account account = (Account) other; + return account.id.equals(this.id); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Media.java b/app/src/main/java/com/keylesspalace/tusky/entity/Media.java index ec6ef31c..1db867fb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Media.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Media.java @@ -3,7 +3,7 @@ package com.keylesspalace.tusky.entity; import com.google.gson.annotations.SerializedName; public class Media { - public int id; + public String id; public String type; diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java index 6964b585..d0874405 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.java @@ -3,7 +3,7 @@ package com.keylesspalace.tusky.entity; import com.google.gson.annotations.SerializedName; public class Relationship { - public int id; + public String id; public boolean following; From 750c1c80a0af3bb3f4d10b78e801d87e12264cb5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 00:27:37 +0100 Subject: [PATCH 05/15] Statuses and notifications loaded/parsed via Retrofit/GSON Notification checker uses since_id as the more exact check-for-updates --- .../keylesspalace/tusky/AccountActivity.java | 1 - .../keylesspalace/tusky/AccountFragment.java | 4 - .../com/keylesspalace/tusky/BaseActivity.java | 3 + .../keylesspalace/tusky/ComposeActivity.java | 1 + .../com/keylesspalace/tusky/MastodonAPI.java | 4 +- .../com/keylesspalace/tusky/Notification.java | 133 ------- .../tusky/NotificationsAdapter.java | 20 +- .../tusky/NotificationsFragment.java | 61 ++- .../tusky/PullNotificationService.java | 132 +++---- .../keylesspalace/tusky/ReportActivity.java | 60 ++- .../com/keylesspalace/tusky/SFragment.java | 37 +- .../java/com/keylesspalace/tusky/Status.java | 357 ------------------ .../tusky/StatusActionListener.java | 2 + .../keylesspalace/tusky/StatusViewHolder.java | 43 ++- .../keylesspalace/tusky/ThreadAdapter.java | 2 + .../keylesspalace/tusky/TimelineAdapter.java | 2 + .../keylesspalace/tusky/TimelineFragment.java | 72 ++-- .../tusky/ViewThreadFragment.java | 82 ++-- .../tusky/entity/Notification.java | 55 +++ .../keylesspalace/tusky/entity/Status.java | 122 ++++++ .../tusky/entity/StatusContext.java | 2 - 21 files changed, 418 insertions(+), 777 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/Notification.java delete mode 100644 app/src/main/java/com/keylesspalace/tusky/Status.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Notification.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/Status.java diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index f517d402..3f5c690a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -75,7 +75,6 @@ public class AccountActivity extends BaseActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_account); - createMastodonAPI(); Intent intent = getIntent(); accountId = intent.getStringExtra("id"); diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java index 35887516..23391fd9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java @@ -184,21 +184,17 @@ public class AccountFragment extends Fragment implements AccountActionListener, } }; - String endpoint; switch (type) { default: case FOLLOWS: { - endpoint = String.format(getString(R.string.endpoint_following), accountId); api.accountFollowing(accountId, fromId, null, null).enqueue(cb); break; } case FOLLOWERS: { - endpoint = String.format(getString(R.string.endpoint_followers), accountId); api.accountFollowers(accountId, fromId, null, null).enqueue(cb); break; } case BLOCKS: { - endpoint = getString(R.string.endpoint_blocks); api.blocks(fromId, null, null).enqueue(cb); break; } diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index 7568dccd..fa529ed9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -51,6 +51,9 @@ public class BaseActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + createMastodonAPI(); + if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) { setTheme(R.style.AppTheme_Light); } diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index 375731de..47b3283f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -76,6 +76,7 @@ import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonObjectRequest; +import com.keylesspalace.tusky.entity.Status; import org.json.JSONArray; import org.json.JSONException; diff --git a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java index 846ad3e3..2cef310e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java +++ b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java @@ -2,7 +2,9 @@ package com.keylesspalace.tusky; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Media; +import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Relationship; +import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.StatusContext; import java.util.List; @@ -146,7 +148,7 @@ public interface MastodonAPI { @Query("limit") Integer limit); @GET("api/v1/favourites") - Call> favourites( + Call> favourites( @Query("max_id") String maxId, @Query("since_id") String sinceId, @Query("limit") Integer limit); diff --git a/app/src/main/java/com/keylesspalace/tusky/Notification.java b/app/src/main/java/com/keylesspalace/tusky/Notification.java deleted file mode 100644 index f7691172..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/Notification.java +++ /dev/null @@ -1,133 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is part of Tusky. - * - * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU - * General Public License as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky. If not, see - * . */ - -package com.keylesspalace.tusky; - -import android.support.annotation.Nullable; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; - -class Notification { - enum Type { - MENTION, - REBLOG, - FAVOURITE, - FOLLOW, - } - private Type type; - private String id; - private String displayName; - private String username; - private String avatar; - private String accountId; - /** Which of the user's statuses has been mentioned, reblogged, or favourited. */ - private Status status; - - private Notification(Type type, String id, String displayName, String username, String avatar, - String accountId) { - this.type = type; - this.id = id; - this.displayName = displayName; - this.username = username; - this.avatar = avatar; - this.accountId = accountId; - } - - Type getType() { - return type; - } - - String getId() { - return id; - } - - String getDisplayName() { - return displayName; - } - - String getUsername() { - return username; - } - - String getAvatar() { - return avatar; - } - - String getAccountId() { - return accountId; - } - - @Nullable Status getStatus() { - return status; - } - - void setStatus(Status status) { - this.status = status; - } - - private boolean hasStatusType() { - return type == Type.MENTION - || type == Type.FAVOURITE - || type == Type.REBLOG; - } - - static List parse(JSONArray array) throws JSONException { - List notifications = new ArrayList<>(); - for (int i = 0; i < array.length(); i++) { - JSONObject object = array.getJSONObject(i); - String id = object.getString("id"); - Notification.Type type = Notification.Type.valueOf( - object.getString("type").toUpperCase()); - JSONObject account = object.getJSONObject("account"); - String displayName = account.getString("display_name"); - if (displayName.isEmpty()) { - displayName = account.getString("username"); - } - String username = account.getString("acct"); - String avatar = account.getString("avatar"); - String accountId = account.getString("id"); - Notification notification = new Notification(type, id, displayName, username, avatar, - accountId); - if (notification.hasStatusType()) { - JSONObject statusObject = object.getJSONObject("status"); - Status status = Status.parse(statusObject, false); - notification.setStatus(status); - } - notifications.add(notification); - } - return notifications; - } - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (this.id == null) { - return this == other; - } else if (!(other instanceof Notification)) { - return false; - } - Notification notification = (Notification) other; - return notification.getId().equals(this.id); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java index c8306542..5b8ea652 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java @@ -28,6 +28,8 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.Status; import com.squareup.picasso.Picasso; import java.util.ArrayList; @@ -86,26 +88,26 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if (position < notifications.size()) { Notification notification = notifications.get(position); - Notification.Type type = notification.getType(); + Notification.Type type = notification.type; switch (type) { case MENTION: { StatusViewHolder holder = (StatusViewHolder) viewHolder; - Status status = notification.getStatus(); + Status status = notification.status; holder.setupWithStatus(status, statusListener); break; } case FAVOURITE: case REBLOG: { StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder; - holder.setMessage(type, notification.getDisplayName(), - notification.getStatus()); + holder.setMessage(type, notification.account.displayName, + notification.status); break; } case FOLLOW: { FollowViewHolder holder = (FollowViewHolder) viewHolder; - holder.setMessage(notification.getDisplayName(), notification.getUsername(), - notification.getAvatar()); - holder.setupButtons(followListener, notification.getAccountId()); + holder.setMessage(notification.account.displayName, notification.account.username, + notification.account.avatar); + holder.setupButtons(followListener, notification.account.id); break; } } @@ -126,7 +128,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe return VIEW_TYPE_FOOTER; } else { Notification notification = notifications.get(position); - switch (notification.getType()) { + switch (notification.type) { default: case MENTION: { return VIEW_TYPE_MENTION; @@ -269,7 +271,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe str.setSpan(new android.text.style.StyleSpan(Typeface.BOLD), 0, displayName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); message.setText(str); - statusContent.setText(status.getContent()); + statusContent.setText(status.content); } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java index 0aeded4d..e6350e4b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java @@ -32,6 +32,8 @@ import com.android.volley.AuthFailureError; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonArrayRequest; +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.Status; import org.json.JSONArray; import org.json.JSONException; @@ -40,6 +42,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import retrofit2.Call; +import retrofit2.Callback; + public class NotificationsFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener, NotificationsAdapter.FollowListener { @@ -92,7 +97,7 @@ public class NotificationsFragment extends SFragment implements NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter(); Notification notification = adapter.getItem(adapter.getItemCount() - 2); if (notification != null) { - sendFetchNotificationsRequest(notification.getId()); + sendFetchNotificationsRequest(notification.id); } else { sendFetchNotificationsRequest(); } @@ -135,37 +140,19 @@ public class NotificationsFragment extends SFragment implements } private void sendFetchNotificationsRequest(final String fromId) { - String endpoint = getString(R.string.endpoint_notifications); - String url = "https://" + domain + endpoint; - if (fromId != null) { - url += "?max_id=" + fromId; - } - JsonArrayRequest request = new JsonArrayRequest(url, - new Response.Listener() { - @Override - public void onResponse(JSONArray response) { - try { - List notifications = Notification.parse(response); - onFetchNotificationsSuccess(notifications, fromId); - } catch (JSONException e) { - onFetchNotificationsFailure(e); - } - } - }, new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onFetchNotificationsFailure(error); - } - }) { + MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI; + + api.notifications(fromId, null, null).enqueue(new Callback>() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call> call, retrofit2.Response> response) { + onFetchNotificationsSuccess(response.body(), fromId); } - }; - request.setTag(TAG); - VolleySingleton.getInstance(getContext()).addToRequestQueue(request); + + @Override + public void onFailure(Call> call, Throwable t) { + onFetchNotificationsFailure((Exception) t); + } + }); } private void sendFetchNotificationsRequest() { @@ -174,7 +161,7 @@ public class NotificationsFragment extends SFragment implements private static boolean findNotification(List notifications, String id) { for (Notification notification : notifications) { - if (notification.getId().equals(id)) { + if (notification.id.equals(id)) { return true; } } @@ -218,7 +205,7 @@ public class NotificationsFragment extends SFragment implements public void onLoadMore() { Notification notification = adapter.getItem(adapter.getItemCount() - 2); if (notification != null) { - sendFetchNotificationsRequest(notification.getId()); + sendFetchNotificationsRequest(notification.id); } else { sendFetchNotificationsRequest(); } @@ -226,22 +213,22 @@ public class NotificationsFragment extends SFragment implements public void onReply(int position) { Notification notification = adapter.getItem(position); - super.reply(notification.getStatus()); + super.reply(notification.status); } public void onReblog(boolean reblog, int position) { Notification notification = adapter.getItem(position); - super.reblog(notification.getStatus(), reblog, adapter, position); + super.reblog(notification.status, reblog, adapter, position); } public void onFavourite(boolean favourite, int position) { Notification notification = adapter.getItem(position); - super.favourite(notification.getStatus(), favourite, adapter, position); + super.favourite(notification.status, favourite, adapter, position); } public void onMore(View view, int position) { Notification notification = adapter.getItem(position); - super.more(notification.getStatus(), view, adapter, position); + super.more(notification.status, view, adapter, position); } public void onViewMedia(String url, Status.MediaAttachment.Type type) { @@ -250,7 +237,7 @@ public class NotificationsFragment extends SFragment implements public void onViewThread(int position) { Notification notification = adapter.getItem(position); - super.viewThread(notification.getStatus()); + super.viewThread(notification.status); } public void onViewTag(String tag) { diff --git a/app/src/main/java/com/keylesspalace/tusky/PullNotificationService.java b/app/src/main/java/com/keylesspalace/tusky/PullNotificationService.java index d562275a..292cc2cd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/PullNotificationService.java +++ b/app/src/main/java/com/keylesspalace/tusky/PullNotificationService.java @@ -26,22 +26,36 @@ import android.provider.Settings; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; +import android.text.Spanned; import com.android.volley.AuthFailureError; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.ImageRequest; import com.android.volley.toolbox.JsonArrayRequest; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.keylesspalace.tusky.entity.*; +import com.keylesspalace.tusky.entity.Notification; import org.json.JSONArray; import org.json.JSONException; +import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + public class PullNotificationService extends IntentService { private static final int NOTIFY_ID = 6; // This is an arbitrary number. private static final String TAG = "PullNotifications"; // logging tag and Volley request tag @@ -62,82 +76,80 @@ public class PullNotificationService extends IntentService { getString(R.string.preferences_file_key), Context.MODE_PRIVATE); String domain = preferences.getString("domain", null); String accessToken = preferences.getString("accessToken", null); - long date = preferences.getLong("lastUpdate", 0); - Date lastUpdate = null; - if (date != 0) { - lastUpdate = new Date(date); - } - checkNotifications(domain, accessToken, lastUpdate); + String lastUpdateId = preferences.getString("lastUpdateId", null); + checkNotifications(domain, accessToken, lastUpdateId); } private void checkNotifications(final String domain, final String accessToken, - final Date lastUpdate) { - String endpoint = getString(R.string.endpoint_notifications); - String url = "https://" + domain + endpoint; - JsonArrayRequest request = new JsonArrayRequest(url, - new Response.Listener() { + final String lastUpdateId) { + OkHttpClient okHttpClient = new OkHttpClient.Builder() + .addInterceptor(new Interceptor() { @Override - public void onResponse(JSONArray response) { - List notifications; - try { - notifications = Notification.parse(response); - } catch (JSONException e) { - onCheckNotificationsFailure(e); - return; - } - onCheckNotificationsSuccess(notifications, lastUpdate); + public okhttp3.Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + + Request.Builder builder = originalRequest.newBuilder() + .header("Authorization", String.format("Bearer %s", accessToken)); + + Request newRequest = builder.build(); + + return chain.proceed(newRequest); } - }, new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onCheckNotificationsFailure(error); - } - }) { + }) + .build(); + + Gson gson = new GsonBuilder() + .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) + .create(); + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + domain) + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build(); + + MastodonAPI api = retrofit.create(MastodonAPI.class); + + api.notifications(null, lastUpdateId, null).enqueue(new Callback>() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call> call, retrofit2.Response> response) { + onCheckNotificationsSuccess(response.body(), lastUpdateId); } - }; - request.setTag(TAG); - VolleySingleton.getInstance(this).addToRequestQueue(request); + + @Override + public void onFailure(Call> call, Throwable t) { + onCheckNotificationsFailure((Exception) t); + } + }); } - private void onCheckNotificationsSuccess(List notifications, Date lastUpdate) { - Date newest = null; + private void onCheckNotificationsSuccess(List notifications, String lastUpdateId) { List mentions = new ArrayList<>(); - for (Notification notification : notifications) { - if (notification.getType() == Notification.Type.MENTION) { - Status status = notification.getStatus(); + + for (com.keylesspalace.tusky.entity.Notification notification : notifications) { + if (notification.type == com.keylesspalace.tusky.entity.Notification.Type.MENTION) { + Status status = notification.status; + if (status != null) { - Date createdAt = status.getCreatedAt(); - if (lastUpdate == null || createdAt.after(lastUpdate)) { - MentionResult mention = new MentionResult(); - mention.content = status.getContent().toString(); - mention.displayName = notification.getDisplayName(); - mention.avatarUrl = status.getAvatar(); - mentions.add(mention); - } - if (newest == null || createdAt.after(newest)) { - newest = createdAt; - } + MentionResult mention = new MentionResult(); + mention.content = status.content.toString(); + mention.displayName = notification.account.displayName; + mention.avatarUrl = status.account.avatar; + mentions.add(mention); } } } - long now = new Date().getTime(); - if (mentions.size() > 0) { + + if (notifications.size() > 0) { SharedPreferences preferences = getSharedPreferences( getString(R.string.preferences_file_key), Context.MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); - editor.putLong("lastUpdate", now); + editor.putString("lastUpdateId", notifications.get(0).id); editor.apply(); + } + + if (mentions.size() > 0) { loadAvatar(mentions, mentions.get(0).avatarUrl); - } else if (newest != null) { - long hoursAgo = (now - newest.getTime()) / (60 * 60 * 1000); - if (hoursAgo >= 1) { - dismissStaleNotifications(); - } } } @@ -227,10 +239,4 @@ public class PullNotificationService extends IntentService { (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFY_ID, builder.build()); } - - private void dismissStaleNotifications() { - NotificationManager notificationManager = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(NOTIFY_ID); - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java b/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java index fb0a8ccd..34aa8694 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java @@ -38,6 +38,7 @@ import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonArrayRequest; import com.android.volley.toolbox.JsonObjectRequest; +import com.keylesspalace.tusky.entity.Status; import org.json.JSONArray; import org.json.JSONException; @@ -48,6 +49,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import retrofit2.Call; +import retrofit2.Callback; + public class ReportActivity extends BaseActivity { private static final String TAG = "ReportActivity"; // logging tag and Volley request tag @@ -197,46 +201,26 @@ public class ReportActivity extends BaseActivity { } private void fetchRecentStatuses(String accountId) { - String endpoint = String.format(getString(R.string.endpoint_statuses), accountId); - String url = "https://" + domain + endpoint; - JsonArrayRequest request = new JsonArrayRequest(url, - new Response.Listener() { - @Override - public void onResponse(JSONArray response) { - List statusList; - try { - statusList = Status.parse(response); - } catch (JSONException e) { - onFetchStatusesFailure(e); - return; - } - // Add all the statuses except reblogs. - List itemList = new ArrayList<>(); - for (Status status : statusList) { - if (status.getRebloggedByDisplayName() == null) { - ReportAdapter.ReportStatus item = new ReportAdapter.ReportStatus( - status.getId(), status.getContent(), false); - itemList.add(item); - } - } - adapter.addItems(itemList); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onFetchStatusesFailure(error); - } - }) { + mastodonAPI.accountStatuses(accountId, null, null, null).enqueue(new Callback>() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call> call, retrofit2.Response> response) { + List statusList = response.body(); + List itemList = new ArrayList<>(); + for (Status status : statusList) { + if (status.reblog != null) { + ReportAdapter.ReportStatus item = new ReportAdapter.ReportStatus( + status.id, status.content, false); + itemList.add(item); + } + } + adapter.addItems(itemList); } - }; - request.setTag(TAG); - VolleySingleton.getInstance(this).addToRequestQueue(request); + + @Override + public void onFailure(Call> call, Throwable t) { + onFetchStatusesFailure((Exception) t); + } + }); } private void onFetchStatusesFailure(Exception exception) { diff --git a/app/src/main/java/com/keylesspalace/tusky/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/SFragment.java index ba1fea6e..58e23ce4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/SFragment.java @@ -33,6 +33,7 @@ import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonObjectRequest; +import com.keylesspalace.tusky.entity.Status; import org.json.JSONObject; @@ -111,24 +112,24 @@ public class SFragment extends Fragment { } protected void reply(Status status) { - String inReplyToId = status.getId(); - Status.Mention[] mentions = status.getMentions(); + String inReplyToId = status.getActionableId(); + Status.Mention[] mentions = status.mentions; List mentionedUsernames = new ArrayList<>(); for (Status.Mention mention : mentions) { - mentionedUsernames.add(mention.getUsername()); + mentionedUsernames.add(mention.username); } - mentionedUsernames.add(status.getUsername()); + mentionedUsernames.add(status.account.username); mentionedUsernames.remove(loggedInUsername); Intent intent = new Intent(getContext(), ComposeActivity.class); intent.putExtra("in_reply_to_id", inReplyToId); - intent.putExtra("reply_visibility", status.getVisibility().toString().toLowerCase()); + intent.putExtra("reply_visibility", status.visibility.toString().toLowerCase()); intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0])); startActivity(intent); } protected void reblog(final Status status, final boolean reblog, final RecyclerView.Adapter adapter, final int position) { - String id = status.getId(); + String id = status.getActionableId(); String endpoint; if (reblog) { endpoint = String.format(getString(R.string.endpoint_reblog), id); @@ -139,7 +140,7 @@ public class SFragment extends Fragment { new Response.Listener() { @Override public void onResponse(JSONObject response) { - status.setReblogged(reblog); + status.reblogged = reblog; adapter.notifyItemChanged(position); } }, null); @@ -147,7 +148,7 @@ public class SFragment extends Fragment { protected void favourite(final Status status, final boolean favourite, final RecyclerView.Adapter adapter, final int position) { - String id = status.getId(); + String id = status.getActionableId(); String endpoint; if (favourite) { endpoint = String.format(getString(R.string.endpoint_favourite), id); @@ -157,7 +158,7 @@ public class SFragment extends Fragment { sendRequest(Request.Method.POST, endpoint, null, new Response.Listener() { @Override public void onResponse(JSONObject response) { - status.setFavourited(favourite); + status.favourited = favourite; adapter.notifyItemChanged(position); } }, null); @@ -180,10 +181,10 @@ public class SFragment extends Fragment { protected void more(Status status, View view, final AdapterItemRemover adapter, final int position) { - final String id = status.getId(); - final String accountId = status.getAccountId(); - final String accountUsename = status.getUsername(); - final Spanned content = status.getContent(); + final String id = status.getActionableId(); + final String accountId = status.getActionableStatus().account.id; + final String accountUsename = status.getActionableStatus().account.username; + final Spanned content = status.getActionableStatus().content; PopupMenu popup = new PopupMenu(getContext(), view); // Give a different menu depending on whether this is the user's own toot or not. if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) { @@ -234,12 +235,8 @@ public class SFragment extends Fragment { protected void viewMedia(String url, Status.MediaAttachment.Type type) { switch (type) { case IMAGE: { - Fragment newFragment; - if (fileExtensionMatches(url, "gif")) { - newFragment = ViewGifFragment.newInstance(url); - } else { - newFragment = ViewMediaFragment.newInstance(url); - } + Fragment newFragment = ViewMediaFragment.newInstance(url); + FragmentManager manager = getFragmentManager(); manager.beginTransaction() .add(R.id.overlay_fragment_container, newFragment) @@ -264,7 +261,7 @@ public class SFragment extends Fragment { protected void viewThread(Status status) { Intent intent = new Intent(getContext(), ViewThreadActivity.class); - intent.putExtra("id", status.getId()); + intent.putExtra("id", status.id); startActivity(intent); } diff --git a/app/src/main/java/com/keylesspalace/tusky/Status.java b/app/src/main/java/com/keylesspalace/tusky/Status.java deleted file mode 100644 index 9ecb2d12..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/Status.java +++ /dev/null @@ -1,357 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is part of Tusky. - * - * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU - * General Public License as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky. If not, see - * . */ - -package com.keylesspalace.tusky; - -import android.support.annotation.Nullable; -import android.text.Spanned; - -import com.emojione.Emojione; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -public class Status { - enum Visibility { - PUBLIC, - UNLISTED, - PRIVATE, - } - - private String id; - private String accountId; - private String displayName; - /** the username with the remote domain appended, like @domain.name, if it's a remote account */ - private String username; - /** the main text of the status, marked up with style for links & mentions, etc */ - private Spanned content; - /** the fully-qualified url of the avatar image */ - private String avatar; - private String rebloggedByDisplayName; - /** when the status was initially created */ - private Date createdAt; - /** whether the authenticated user has reblogged this status */ - private boolean reblogged; - /** whether the authenticated user has favourited this status */ - private boolean favourited; - private boolean sensitive; - private String spoilerText; - private Visibility visibility; - private MediaAttachment[] attachments; - private Mention[] mentions; - - static final int MAX_MEDIA_ATTACHMENTS = 4; - - public Status(String id, String accountId, String displayName, String username, Spanned content, - String avatar, Date createdAt, boolean reblogged, boolean favourited, - String visibility) { - this.id = id; - this.accountId = accountId; - this.displayName = displayName; - this.username = username; - this.content = content; - this.avatar = avatar; - this.createdAt = createdAt; - this.reblogged = reblogged; - this.favourited = favourited; - this.spoilerText = ""; - this.visibility = Visibility.valueOf(visibility.toUpperCase()); - this.attachments = new MediaAttachment[0]; - this.mentions = new Mention[0]; - } - - String getId() { - return id; - } - - String getAccountId() { - return accountId; - } - - String getDisplayName() { - return displayName; - } - - String getUsername() { - return username; - } - - Spanned getContent() { - return content; - } - - String getAvatar() { - return avatar; - } - - Date getCreatedAt() { - return createdAt; - } - - String getRebloggedByDisplayName() { - return rebloggedByDisplayName; - } - - boolean getReblogged() { - return reblogged; - } - - boolean getFavourited() { - return favourited; - } - - boolean getSensitive() { - return sensitive; - } - - String getSpoilerText() { - return spoilerText; - } - - Visibility getVisibility() { - return visibility; - } - - MediaAttachment[] getAttachments() { - return attachments; - } - - Mention[] getMentions() { - return mentions; - } - - private void setRebloggedByDisplayName(String name) { - rebloggedByDisplayName = name; - } - - void setReblogged(boolean reblogged) { - this.reblogged = reblogged; - } - - void setFavourited(boolean favourited) { - this.favourited = favourited; - } - - private void setSpoilerText(String spoilerText) { - this.spoilerText = spoilerText; - } - - private void setMentions(Mention[] mentions) { - this.mentions = mentions; - } - - private void setAttachments(MediaAttachment[] attachments, boolean sensitive) { - this.attachments = attachments; - this.sensitive = sensitive; - } - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (this.id == null) { - return this == other; - } else if (!(other instanceof Status)) { - return false; - } - Status status = (Status) other; - return status.id.equals(this.id); - } - - @SuppressWarnings("SimpleDateFormat") // UTC needs to not specify a Locale - @Nullable - private static Date parseDate(String dateTime) { - Date date; - String s = dateTime.replace("Z", "+00:00"); - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - try { - date = format.parse(s); - } catch (ParseException e) { - e.printStackTrace(); - return null; - } - return date; - } - - private static MediaAttachment.Type parseMediaType(@Nullable String type) { - if (type == null) { - return MediaAttachment.Type.UNKNOWN; - } - switch (type.toUpperCase()) { - case "IMAGE": return MediaAttachment.Type.IMAGE; - case "GIFV": - case "VIDEO": return MediaAttachment.Type.VIDEO; - default: return MediaAttachment.Type.UNKNOWN; - } - } - - public static Status parse(JSONObject object, boolean isReblog) throws JSONException { - String id = object.getString("id"); - String content = object.getString("content"); - Date createdAt = parseDate(object.getString("created_at")); - boolean reblogged = object.optBoolean("reblogged"); - boolean favourited = object.optBoolean("favourited"); - String spoilerText = object.getString("spoiler_text"); - boolean sensitive = object.optBoolean("sensitive"); - String visibility = object.getString("visibility"); - - JSONObject account = object.getJSONObject("account"); - String accountId = account.getString("id"); - String displayName = account.getString("display_name"); - if (displayName.isEmpty()) { - displayName = account.getString("username"); - } - String username = account.getString("acct"); - String avatarUrl = account.getString("avatar"); - String avatar; - if (!avatarUrl.equals("/avatars/original/missing.png")) { - avatar = avatarUrl; - } else { - avatar = ""; - } - - JSONArray mentionsArray = object.getJSONArray("mentions"); - Mention[] mentions = null; - if (mentionsArray != null) { - int n = mentionsArray.length(); - mentions = new Mention[n]; - for (int i = 0; i < n; i++) { - JSONObject mention = mentionsArray.getJSONObject(i); - String url = mention.getString("url"); - String mentionedUsername = mention.getString("acct"); - String mentionedAccountId = mention.getString("id"); - mentions[i] = new Mention(url, mentionedUsername, mentionedAccountId); - } - } - - JSONArray mediaAttachments = object.getJSONArray("media_attachments"); - MediaAttachment[] attachments = null; - if (mediaAttachments != null) { - int n = mediaAttachments.length(); - attachments = new MediaAttachment[n]; - for (int i = 0; i < n; i++) { - JSONObject attachment = mediaAttachments.getJSONObject(i); - String url = attachment.getString("url"); - String previewUrl = attachment.getString("preview_url"); - String type = attachment.getString("type"); - attachments[i] = new MediaAttachment(url, previewUrl, parseMediaType(type)); - } - } - - Status reblog = null; - /* This case shouldn't be hit after the first recursion at all. But if this method is - * passed unusual data this check will prevent extra recursion */ - if (!isReblog) { - JSONObject reblogObject = object.optJSONObject("reblog"); - if (reblogObject != null) { - reblog = parse(reblogObject, true); - } - } - - Status status; - if (reblog != null) { - status = reblog; - status.setRebloggedByDisplayName(displayName); - } else { - Spanned contentPlus = HtmlUtils.fromHtml(Emojione.shortnameToUnicode(content, false)); - status = new Status( - id, accountId, displayName, username, contentPlus, avatar, createdAt, - reblogged, favourited, visibility); - if (mentions != null) { - status.setMentions(mentions); - } - if (attachments != null) { - status.setAttachments(attachments, sensitive); - } - if (!spoilerText.isEmpty()) { - status.setSpoilerText(spoilerText); - } - } - return status; - } - - public static List parse(JSONArray array) throws JSONException { - List statuses = new ArrayList<>(); - for (int i = 0; i < array.length(); i++) { - JSONObject object = array.getJSONObject(i); - statuses.add(parse(object, false)); - } - return statuses; - } - - static class MediaAttachment { - enum Type { - IMAGE, - VIDEO, - UNKNOWN, - } - - private String url; - private String previewUrl; - private Type type; - - MediaAttachment(String url, String previewUrl, Type type) { - this.url = url; - this.previewUrl = previewUrl; - this.type = type; - } - - String getUrl() { - return url; - } - - String getPreviewUrl() { - return previewUrl; - } - - Type getType() { - return type; - } - } - - static class Mention { - private String url; - private String username; - private String id; - - Mention(String url, String username, String id) { - this.url = url; - this.username = username; - this.id = id; - } - - String getUrl() { - return url; - } - - String getUsername() { - return username; - } - - String getId() { - return id; - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java index abf0a5b6..548bde06 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java @@ -17,6 +17,8 @@ package com.keylesspalace.tusky; import android.view.View; +import com.keylesspalace.tusky.entity.Status; + interface StatusActionListener { void onReply(int position); void onReblog(final boolean reblog, final int position); diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java index 09521551..ac346b98 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java @@ -30,6 +30,7 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.ToggleButton; +import com.keylesspalace.tusky.entity.Status; import com.squareup.picasso.Picasso; import com.varunest.sparkbutton.SparkButton; import com.varunest.sparkbutton.SparkEventListener; @@ -124,8 +125,8 @@ class StatusViewHolder extends RecyclerView.ViewHolder { final String accountUsername = text.subSequence(1, text.length()).toString(); String id = null; for (Status.Mention mention: mentions) { - if (mention.getUsername().equals(accountUsername)) { - id = mention.getId(); + if (mention.username.equals(accountUsername)) { + id = mention.id; } } if (id != null) { @@ -227,7 +228,7 @@ class StatusViewHolder extends RecyclerView.ViewHolder { final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); for (int i = 0; i < n; i++) { - String previewUrl = attachments[i].getPreviewUrl(); + String previewUrl = attachments[i].previewUrl; previews[i].setVisibility(View.VISIBLE); @@ -236,8 +237,8 @@ class StatusViewHolder extends RecyclerView.ViewHolder { .placeholder(mediaPreviewUnloadedId) .into(previews[i]); - final String url = attachments[i].getUrl(); - final Status.MediaAttachment.Type type = attachments[i].getType(); + final String url = attachments[i].url; + final Status.MediaAttachment.Type type = attachments[i].type; previews[i].setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -339,33 +340,35 @@ class StatusViewHolder extends RecyclerView.ViewHolder { } void setupWithStatus(Status status, StatusActionListener listener) { - setDisplayName(status.getDisplayName()); - setUsername(status.getUsername()); - setCreatedAt(status.getCreatedAt()); - setContent(status.getContent(), status.getMentions(), listener); - setAvatar(status.getAvatar()); - setReblogged(status.getReblogged()); - setFavourited(status.getFavourited()); - String rebloggedByDisplayName = status.getRebloggedByDisplayName(); - if (rebloggedByDisplayName == null) { + Status realStatus = status.getActionableStatus(); + + setDisplayName(realStatus.account.displayName); + setUsername(realStatus.account.username); + setCreatedAt(realStatus.createdAt); + setContent(realStatus.content, realStatus.mentions, listener); + setAvatar(realStatus.account.avatar); + setReblogged(realStatus.reblogged); + setFavourited(realStatus.favourited); + String rebloggedByDisplayName = status.account.displayName; + if (status.reblog == null) { hideRebloggedByDisplayName(); } else { setRebloggedByDisplayName(rebloggedByDisplayName); } - Status.MediaAttachment[] attachments = status.getAttachments(); - boolean sensitive = status.getSensitive(); + Status.MediaAttachment[] attachments = realStatus.attachments; + boolean sensitive = realStatus.sensitive; setMediaPreviews(attachments, sensitive, listener); /* A status without attachments is sometimes still marked sensitive, so it's necessary to * check both whether there are any attachments and if it's marked sensitive. */ if (!sensitive || attachments.length == 0) { hideSensitiveMediaWarning(); } - setupButtons(listener, status.getAccountId()); - setRebloggingEnabled(status.getVisibility() != Status.Visibility.PRIVATE); - if (status.getSpoilerText().isEmpty()) { + setupButtons(listener, realStatus.account.id); + setRebloggingEnabled(realStatus.visibility != Status.Visibility.PRIVATE); + if (realStatus.spoilerText.isEmpty()) { hideSpoilerText(); } else { - setSpoilerText(status.getSpoilerText()); + setSpoilerText(realStatus.spoilerText); } } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java b/app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java index 3e9ed57b..4cc2dc48 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java @@ -20,6 +20,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.keylesspalace.tusky.entity.Status; + import java.util.ArrayList; import java.util.List; diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java index 792afd40..428413cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java @@ -21,6 +21,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.keylesspalace.tusky.entity.Status; + import java.util.ArrayList; import java.util.List; diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java index 0f41e534..907d6c90 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java @@ -31,6 +31,7 @@ import com.android.volley.AuthFailureError; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonArrayRequest; +import com.keylesspalace.tusky.entity.Status; import org.json.JSONArray; import org.json.JSONException; @@ -39,6 +40,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import retrofit2.Call; +import retrofit2.Callback; + public class TimelineFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener { private static final String TAG = "Timeline"; // logging tag and Volley request tag @@ -117,7 +121,7 @@ public class TimelineFragment extends SFragment implements TimelineAdapter adapter = (TimelineAdapter) view.getAdapter(); Status status = adapter.getItem(adapter.getItemCount() - 2); if (status != null) { - sendFetchTimelineRequest(status.getId()); + sendFetchTimelineRequest(status.id); } else { sendFetchTimelineRequest(); } @@ -168,67 +172,43 @@ public class TimelineFragment extends SFragment implements } private void sendFetchTimelineRequest(final String fromId) { - String endpoint; + MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI; + + Callback> cb = new Callback>() { + @Override + public void onResponse(Call> call, retrofit2.Response> response) { + onFetchTimelineSuccess(response.body(), fromId); + } + + @Override + public void onFailure(Call> call, Throwable t) { + onFetchTimelineFailure((Exception) t); + } + }; + switch (kind) { default: case HOME: { - endpoint = getString(R.string.endpoint_timelines_home); - break; - } - case MENTIONS: { - endpoint = getString(R.string.endpoint_timelines_mentions); + api.homeTimeline(fromId, null, null).enqueue(cb); break; } case PUBLIC: { - endpoint = getString(R.string.endpoint_timelines_public); + api.publicTimeline(null, fromId, null, null).enqueue(cb); break; } case TAG: { - endpoint = String.format(getString(R.string.endpoint_timelines_tag), hashtagOrId); + api.hashtagTimeline(hashtagOrId, null, fromId, null, null).enqueue(cb); break; } case USER: { - endpoint = String.format(getString(R.string.endpoint_statuses), hashtagOrId); + api.accountStatuses(hashtagOrId, fromId, null, null).enqueue(cb); break; } case FAVOURITES: { - endpoint = getString(R.string.endpoint_favourites); + api.favourites(fromId, null, null).enqueue(cb); break; } } - String url = "https://" + domain + endpoint; - if (fromId != null) { - url += "?max_id=" + fromId; - } - JsonArrayRequest request = new JsonArrayRequest(url, - new Response.Listener() { - @Override - public void onResponse(JSONArray response) { - List statuses = null; - try { - statuses = Status.parse(response); - } catch (JSONException e) { - onFetchTimelineFailure(e); - } - if (statuses != null) { - onFetchTimelineSuccess(statuses, fromId); - } - } - }, new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onFetchTimelineFailure(error); - } - }) { - @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; - } - }; - request.setTag(TAG); - VolleySingleton.getInstance(getContext()).addToRequestQueue(request); } private void sendFetchTimelineRequest() { @@ -237,7 +217,7 @@ public class TimelineFragment extends SFragment implements private static boolean findStatus(List statuses, String id) { for (Status status : statuses) { - if (status.getId().equals(id)) { + if (status.id.equals(id)) { return true; } } @@ -281,7 +261,7 @@ public class TimelineFragment extends SFragment implements public void onLoadMore() { Status status = adapter.getItem(adapter.getItemCount() - 2); if (status != null) { - sendFetchTimelineRequest(status.getId()); + sendFetchTimelineRequest(status.id); } else { sendFetchTimelineRequest(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java index 9389c0bf..f615eb04 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java @@ -30,12 +30,17 @@ import android.view.ViewGroup; import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.StatusContext; import org.json.JSONException; import org.json.JSONObject; import java.util.List; +import retrofit2.Call; +import retrofit2.Callback; + public class ViewThreadFragment extends SFragment implements StatusActionListener { private RecyclerView recyclerView; private ThreadAdapter adapter; @@ -78,54 +83,39 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene } private void sendStatusRequest(final String id) { - String endpoint = String.format(getString(R.string.endpoint_get_status), id); - super.sendRequest(Request.Method.GET, endpoint, null, - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - Status status; - try { - status = Status.parse(response, false); - } catch (JSONException e) { - onThreadRequestFailure(id); - return; - } - int position = adapter.insertStatus(status); - recyclerView.scrollToPosition(position); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onThreadRequestFailure(id); - } - }); + MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI; + + api.status(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, retrofit2.Response response) { + int position = adapter.insertStatus(response.body()); + recyclerView.scrollToPosition(position); + } + + @Override + public void onFailure(Call call, Throwable t) { + onThreadRequestFailure(id); + } + }); } private void sendThreadRequest(final String id) { - String endpoint = String.format(getString(R.string.endpoint_context), id); - super.sendRequest(Request.Method.GET, endpoint, null, - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - try { - List ancestors = - Status.parse(response.getJSONArray("ancestors")); - List descendants = - Status.parse(response.getJSONArray("descendants")); - adapter.addAncestors(ancestors); - adapter.addDescendants(descendants); - } catch (JSONException e) { - onThreadRequestFailure(id); - } - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onThreadRequestFailure(id); - } - }); + MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI; + + api.statusContext(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, retrofit2.Response response) { + StatusContext context = response.body(); + + adapter.addAncestors(context.ancestors); + adapter.addDescendants(context.descendants); + } + + @Override + public void onFailure(Call call, Throwable t) { + onThreadRequestFailure(id); + } + }); } private void onThreadRequestFailure(final String id) { @@ -162,7 +152,7 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene public void onViewThread(int position) { Status status = adapter.getItem(position); - if (thisThreadsStatusId.equals(status.getId())) { + if (thisThreadsStatusId.equals(status.id)) { // If already viewing this thread, don't reopen it. return; } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.java b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.java new file mode 100644 index 00000000..fb0b7b9a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.java @@ -0,0 +1,55 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky.entity; + +import com.google.gson.annotations.SerializedName; + +public class Notification { + public enum Type { + @SerializedName("mention") + MENTION, + @SerializedName("reblog") + REBLOG, + @SerializedName("favourite") + FAVOURITE, + @SerializedName("follow") + FOLLOW, + } + + public Type type; + + public String id; + + public Account account; + + public Status status; + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (this.id == null) { + return this == other; + } else if (!(other instanceof Notification)) { + return false; + } + Notification notification = (Notification) other; + return notification.id.equals(this.id); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java new file mode 100644 index 00000000..72c6682e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java @@ -0,0 +1,122 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky.entity; + +import android.text.Spanned; + +import com.google.gson.annotations.SerializedName; + +import java.util.Date; + +public class Status { + private Status actionableStatus; + + public String getActionableId() { + return reblog == null ? id : reblog.id; + } + + public Status getActionableStatus() { + return reblog == null ? this : reblog; + } + + public enum Visibility { + @SerializedName("public") + PUBLIC, + @SerializedName("unlisted") + UNLISTED, + @SerializedName("private") + PRIVATE, + } + + public String id; + + public Account account; + + public Spanned content; + + public Status reblog; + + @SerializedName("created_at") + public Date createdAt; + + public boolean reblogged; + + public boolean favourited; + + public boolean sensitive; + + @SerializedName("spoiler_text") + public String spoilerText; + + public Visibility visibility; + + @SerializedName("media_attachments") + public MediaAttachment[] attachments; + + public Mention[] mentions; + + public static final int MAX_MEDIA_ATTACHMENTS = 4; + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (this.id == null) { + return this == other; + } else if (!(other instanceof Status)) { + return false; + } + Status status = (Status) other; + return status.id.equals(this.id); + } + + public static class MediaAttachment { + public enum Type { + @SerializedName("image") + IMAGE, + @SerializedName("gifv") + GIFV, + @SerializedName("video") + VIDEO, + UNKNOWN, + } + + public String url; + + @SerializedName("preview_url") + public String previewUrl; + + @SerializedName("text_url") + public String textUrl; + + @SerializedName("remote_url") + public String remoteUrl; + + public Type type; + } + + public static class Mention { + public String id; + + public String url; + + @SerializedName("acct") + public String username; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java index 4099df6b..338b5503 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.java @@ -1,7 +1,5 @@ package com.keylesspalace.tusky.entity; -import com.keylesspalace.tusky.Status; - import java.util.List; public class StatusContext { From 8035fba22c601f84e65d8ae843d2042b8aac7390 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 00:46:13 +0100 Subject: [PATCH 06/15] Use retrofit methods for status actions --- .../com/keylesspalace/tusky/SFragment.java | 102 +++++++----------- 1 file changed, 39 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/SFragment.java index 58e23ce4..7d4feda3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/SFragment.java @@ -42,6 +42,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import retrofit2.Call; +import retrofit2.Callback; + /* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an * awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature * of that is complicated by how they're coupled with Status and Notification and the corresponding @@ -55,6 +58,7 @@ public class SFragment extends Fragment { protected String accessToken; protected String loggedInAccountId; protected String loggedInUsername; + private MastodonAPI api; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -66,6 +70,7 @@ public class SFragment extends Fragment { accessToken = preferences.getString("accessToken", null); loggedInAccountId = preferences.getString("loggedInAccountId", null); loggedInUsername = preferences.getString("loggedInAccountUsername", null); + api = ((BaseActivity) getActivity()).mastodonAPI; } @Override @@ -74,43 +79,6 @@ public class SFragment extends Fragment { super.onDestroy(); } - protected void sendRequest( - int method, String endpoint, JSONObject parameters, - @Nullable Response.Listener responseListener, - @Nullable Response.ErrorListener errorListener) { - if (responseListener == null) { - // Use a dummy listener if one wasn't specified so the request can be constructed. - responseListener = new Response.Listener() { - @Override - public void onResponse(JSONObject response) {} - }; - } - if (errorListener == null) { - errorListener = new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - Log.e(TAG, "Request Failed: " + error.getMessage()); - } - }; - } - String url = "https://" + domain + endpoint; - JsonObjectRequest request = new JsonObjectRequest( - method, url, parameters, responseListener, errorListener) { - @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; - } - }; - request.setTag(TAG); - VolleySingleton.getInstance(getContext()).addToRequestQueue(request); - } - - protected void postRequest(String endpoint) { - sendRequest(Request.Method.POST, endpoint, null, null, null); - } - protected void reply(Status status) { String inReplyToId = status.getActionableId(); Status.Mention[] mentions = status.mentions; @@ -130,53 +98,61 @@ public class SFragment extends Fragment { protected void reblog(final Status status, final boolean reblog, final RecyclerView.Adapter adapter, final int position) { String id = status.getActionableId(); - String endpoint; + + Callback cb = new Callback() { + @Override + public void onResponse(Call call, retrofit2.Response response) { + status.reblogged = reblog; + adapter.notifyItemChanged(position); + } + + @Override + public void onFailure(Call call, Throwable t) { + + } + }; + if (reblog) { - endpoint = String.format(getString(R.string.endpoint_reblog), id); + api.reblogStatus(id).enqueue(cb); } else { - endpoint = String.format(getString(R.string.endpoint_unreblog), id); + api.unreblogStatus(id).enqueue(cb); } - sendRequest(Request.Method.POST, endpoint, null, - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - status.reblogged = reblog; - adapter.notifyItemChanged(position); - } - }, null); } protected void favourite(final Status status, final boolean favourite, final RecyclerView.Adapter adapter, final int position) { String id = status.getActionableId(); - String endpoint; - if (favourite) { - endpoint = String.format(getString(R.string.endpoint_favourite), id); - } else { - endpoint = String.format(getString(R.string.endpoint_unfavourite), id); - } - sendRequest(Request.Method.POST, endpoint, null, new Response.Listener() { + + Callback cb = new Callback() { @Override - public void onResponse(JSONObject response) { + public void onResponse(Call call, retrofit2.Response response) { status.favourited = favourite; adapter.notifyItemChanged(position); } - }, null); + + @Override + public void onFailure(Call call, Throwable t) { + + } + }; + + if (favourite) { + api.favouriteStatus(id).enqueue(cb); + } else { + api.unfavouriteStatus(id).enqueue(cb); + } } protected void follow(String id) { - String endpoint = String.format(getString(R.string.endpoint_follow), id); - postRequest(endpoint); + api.followAccount(id).enqueue(null); } private void block(String id) { - String endpoint = String.format(getString(R.string.endpoint_block), id); - postRequest(endpoint); + api.blockAccount(id).enqueue(null); } private void delete(String id) { - String endpoint = String.format(getString(R.string.endpoint_delete), id); - sendRequest(Request.Method.DELETE, endpoint, null, null, null); + api.deleteStatus(id).enqueue(null); } protected void more(Status status, View view, final AdapterItemRemover adapter, From f938dff9ed30e3f0c92516aa5535addc53609af2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 01:01:45 +0100 Subject: [PATCH 07/15] Use retrofit methods for profile actions --- .../keylesspalace/tusky/AccountActivity.java | 147 +++++------------- .../com/keylesspalace/tusky/MastodonAPI.java | 2 +- 2 files changed, 43 insertions(+), 106 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index 3f5c690a..a4b30f62 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -39,22 +39,13 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.android.volley.AuthFailureError; -import com.android.volley.Request; -import com.android.volley.Response; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.JsonArrayRequest; -import com.android.volley.toolbox.JsonObjectRequest; import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.entity.Relationship; import com.pkmmte.view.CircularImageView; import com.squareup.picasso.Picasso; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; import retrofit2.Call; import retrofit2.Callback; @@ -247,40 +238,20 @@ public class AccountActivity extends BaseActivity { } private void obtainRelationships() { - String endpoint = getString(R.string.endpoint_relationships); - String url = String.format("https://%s%s?id=%s", domain, endpoint, accountId); - JsonArrayRequest request = new JsonArrayRequest(url, - new Response.Listener() { - @Override - public void onResponse(JSONArray response) { - boolean following; - boolean blocking; - try { - JSONObject object = response.getJSONObject(0); - following = object.getBoolean("following"); - blocking = object.getBoolean("blocking"); - } catch (JSONException e) { - onObtainRelationshipsFailure(e); - return; - } - onObtainRelationshipsSuccess(following, blocking); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onObtainRelationshipsFailure(error); - } - }) { + List ids = new ArrayList<>(1); + ids.add(accountId); + mastodonAPI.relationships(ids).enqueue(new Callback>() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call> call, retrofit2.Response> response) { + Relationship relationship = response.body().get(0); + onObtainRelationshipsSuccess(relationship.following, relationship.blocking); } - }; - request.setTag(TAG); - VolleySingleton.getInstance(this).addToRequestQueue(request); + + @Override + public void onFailure(Call> call, Throwable t) { + onObtainRelationshipsFailure((Exception) t); + } + }); } private void onObtainRelationshipsSuccess(boolean following, boolean blocking) { @@ -347,50 +318,26 @@ public class AccountActivity extends BaseActivity { return super.onPrepareOptionsMenu(menu); } - private void postRequest(String endpoint, Response.Listener listener, - Response.ErrorListener errorListener) { - String url = "https://" + domain + endpoint; - JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, null, listener, - errorListener) { + private void follow(final String id) { + Callback cb = new Callback() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call call, retrofit2.Response response) { + following = response.body().following; + // TODO: display message/indicator when "requested" is true (i.e. when the follow is awaiting approval) + updateButtons(); + } + + @Override + public void onFailure(Call call, Throwable t) { + onFollowFailure(id); } }; - request.setTag(TAG); - VolleySingleton.getInstance(this).addToRequestQueue(request); - } - private void follow(final String id) { - int endpointId; if (following) { - endpointId = R.string.endpoint_unfollow; + mastodonAPI.unfollowAccount(id).enqueue(cb); } else { - endpointId = R.string.endpoint_follow; + mastodonAPI.followAccount(id).enqueue(cb); } - postRequest(String.format(getString(endpointId), id), - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - boolean followingValue; - try { - followingValue = response.getBoolean("following"); - } catch (JSONException e) { - onFollowFailure(id); - return; - } - following = followingValue; - updateButtons(); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onFollowFailure(id); - } - }); } private void onFollowFailure(final String id) { @@ -412,33 +359,23 @@ public class AccountActivity extends BaseActivity { } private void block(final String id) { - int endpointId; + Callback cb = new Callback() { + @Override + public void onResponse(Call call, retrofit2.Response response) { + blocking = response.body().blocking; + updateButtons(); + } + + @Override + public void onFailure(Call call, Throwable t) { + onBlockFailure(id); + } + }; if (blocking) { - endpointId = R.string.endpoint_unblock; + mastodonAPI.unblockAccount(id).enqueue(cb); } else { - endpointId = R.string.endpoint_block; + mastodonAPI.blockAccount(id).enqueue(cb); } - postRequest(String.format(getString(endpointId), id), - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - boolean blockingValue; - try { - blockingValue = response.getBoolean("blocking"); - } catch (JSONException e) { - onBlockFailure(id); - return; - } - blocking = blockingValue; - updateButtons(); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onBlockFailure(id); - } - }); } private void onBlockFailure(final String id) { diff --git a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java index 2cef310e..9bcf7057 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java +++ b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java @@ -133,7 +133,7 @@ public interface MastodonAPI { Call unmuteAccount(@Path("id") String accountId); @GET("api/v1/accounts/relationships") - Call> relationships(@Query("id[]") List accountIds); + Call> relationships(@Query("id[]") List accountIds); @GET("api/v1/blocks") Call> blocks( From d09e9706b67da18808f0641c0af664141a4c1c1c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 16:59:18 +0100 Subject: [PATCH 08/15] Replace remaining non-retrofit API calls Only exception is the OAuth2-related calls --- .../keylesspalace/tusky/AccountFragment.java | 45 ++---- .../keylesspalace/tusky/ComposeActivity.java | 147 +++++++----------- .../com/keylesspalace/tusky/MainActivity.java | 46 ++---- .../com/keylesspalace/tusky/MastodonAPI.java | 7 +- .../keylesspalace/tusky/ReportActivity.java | 47 ++---- .../com/keylesspalace/tusky/SFragment.java | 38 ++++- .../tusky/SpannedTypeAdapter.java | 3 +- 7 files changed, 141 insertions(+), 192 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java index 23391fd9..09115c60 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java @@ -36,6 +36,7 @@ import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.StringRequest; import com.keylesspalace.tusky.entity.Account; +import com.keylesspalace.tusky.entity.Relationship; import java.util.HashMap; import java.util.List; @@ -63,6 +64,7 @@ public class AccountFragment extends Fragment implements AccountActionListener, private EndlessOnScrollListener scrollListener; private AccountAdapter adapter; private TabLayout.OnTabSelectedListener onTabSelectedListener; + private MastodonAPI api; public static AccountFragment newInstance(Type type) { Bundle arguments = new Bundle(); @@ -92,6 +94,7 @@ public class AccountFragment extends Fragment implements AccountActionListener, getString(R.string.preferences_file_key), Context.MODE_PRIVATE); domain = preferences.getString("domain", null); accessToken = preferences.getString("accessToken", null); + api = ((BaseActivity) getActivity()).mastodonAPI; } @Override @@ -170,8 +173,6 @@ public class AccountFragment extends Fragment implements AccountActionListener, } private void fetchAccounts(final String fromId) { - MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI; - Callback> cb = new Callback>() { @Override public void onResponse(Call> call, retrofit2.Response> response) { @@ -265,35 +266,23 @@ public class AccountFragment extends Fragment implements AccountActionListener, } public void onBlock(final boolean block, final String id, final int position) { - String endpoint; - if (!block) { - endpoint = String.format(getString(R.string.endpoint_unblock), id); - } else { - endpoint = String.format(getString(R.string.endpoint_block), id); - } - String url = "https://" + domain + endpoint; - StringRequest request = new StringRequest(Request.Method.POST, url, - new Response.Listener() { - @Override - public void onResponse(String response) { - onBlockSuccess(block, position); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onBlockFailure(block, id); - } - }) { + Callback cb = new Callback() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call call, retrofit2.Response response) { + onBlockSuccess(block, position); + } + + @Override + public void onFailure(Call call, Throwable t) { + onBlockFailure(block, id); } }; - request.setTag(TAG); - VolleySingleton.getInstance(getContext()).addToRequestQueue(request); + + if (!block) { + api.unblockAccount(id).enqueue(cb); + } else { + api.blockAccount(id).enqueue(cb); + } } private void onBlockSuccess(boolean blocked, int position) { diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index 47b3283f..4c8f2154 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -76,6 +76,7 @@ import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonObjectRequest; +import com.keylesspalace.tusky.entity.Media; import com.keylesspalace.tusky.entity.Status; import org.json.JSONArray; @@ -96,6 +97,12 @@ import java.util.Locale; import java.util.Map; import java.util.Random; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import retrofit2.Call; +import retrofit2.Callback; + public class ComposeActivity extends BaseActivity { private static final String TAG = "ComposeActivity"; // logging tag, and volley request tag private static final int STATUS_CHARACTER_LIMIT = 500; @@ -138,7 +145,7 @@ public class ComposeActivity extends BaseActivity { ImageView preview; Uri uri; String id; - Request uploadRequest; + Call uploadRequest; ReadyStage readyStage; byte[] content; long mediaSize; @@ -630,53 +637,28 @@ public class ComposeActivity extends BaseActivity { private void sendStatus(String content, String visibility, boolean sensitive, String spoilerText) { - String endpoint = getString(R.string.endpoint_status); - String url = "https://" + domain + endpoint; - JSONObject parameters = new JSONObject(); - try { - parameters.put("status", content); - parameters.put("visibility", visibility); - parameters.put("sensitive", sensitive); - parameters.put("spoiler_text", spoilerText); - if (inReplyToId != null) { - parameters.put("in_reply_to_id", inReplyToId); - } - JSONArray mediaIds = new JSONArray(); - for (QueuedMedia item : mediaQueued) { - mediaIds.put(item.id); - } - if (mediaIds.length() > 0) { - parameters.put("media_ids", mediaIds); - } - } catch (JSONException e) { - onSendFailure(); - return; + ArrayList mediaIds = new ArrayList(); + + for (QueuedMedia item : mediaQueued) { + mediaIds.add(item.id); } - JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, parameters, - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - onSendSuccess(); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onSendFailure(); - } - }) { + + mastodonAPI.createStatus(content, inReplyToId, spoilerText, visibility, sensitive, mediaIds).enqueue(new Callback() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call call, retrofit2.Response response) { + onSendSuccess(); } - }; - VolleySingleton.getInstance(this).addToRequestQueue(request); + + @Override + public void onFailure(Call call, Throwable t) { + onSendFailure(); + } + }); } private void onSendSuccess() { - Toast.makeText(this, getString(R.string.confirmation_send), Toast.LENGTH_SHORT).show(); + Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), getString(R.string.confirmation_send), Snackbar.LENGTH_SHORT); + bar.show(); finish(); } @@ -942,9 +924,6 @@ public class ComposeActivity extends BaseActivity { private void uploadMedia(final QueuedMedia item) { item.readyStage = QueuedMedia.ReadyStage.UPLOADING; - String endpoint = getString(R.string.endpoint_media); - String url = "https://" + domain + endpoint; - final String mimeType = getContentResolver().getType(item.uri); MimeTypeMap map = MimeTypeMap.getSingleton(); String fileExtension = map.getExtensionFromMimeType(mimeType); @@ -954,58 +933,42 @@ public class ComposeActivity extends BaseActivity { randomAlphanumericString(10), fileExtension); - MultipartRequest request = new MultipartRequest(Request.Method.POST, url, null, - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - try { - item.id = response.getString("id"); - } catch (JSONException e) { - onUploadFailure(item); - return; - } - waitForMediaLatch.countDown(); - } - }, new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onUploadFailure(item); - } - }) { + byte[] content = item.content; + + if (content == null) { + InputStream stream; + + try { + stream = getContentResolver().openInputStream(item.uri); + } catch (FileNotFoundException e) { + return; + } + + content = inputStreamGetBytes(stream); + IOUtils.closeQuietly(stream); + + if (content == null) { + return; + } + } + + RequestBody requestFile = RequestBody.create(MediaType.parse(mimeType), content); + MultipartBody.Part body = MultipartBody.Part.createFormData("file", filename, requestFile); + + item.uploadRequest = mastodonAPI.uploadMedia(body); + + item.uploadRequest.enqueue(new Callback() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call call, retrofit2.Response response) { + item.id = response.body().id; + waitForMediaLatch.countDown(); } @Override - public DataItem getData() { - byte[] content = item.content; - if (content == null) { - InputStream stream; - try { - stream = getContentResolver().openInputStream(item.uri); - } catch (FileNotFoundException e) { - return null; - } - content = inputStreamGetBytes(stream); - IOUtils.closeQuietly(stream); - if (content == null) { - return null; - } - } - DataItem data = new DataItem(); - data.name = "file"; - data.filename = filename; - data.mimeType = mimeType; - data.content = content; - return data; + public void onFailure(Call call, Throwable t) { + onUploadFailure(item); } - }; - request.setTag(TAG); - item.uploadRequest = request; - VolleySingleton.getInstance(this).addToRequestQueue(request); + }); } private void onUploadFailure(QueuedMedia item) { diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 3fb5b338..37f8e7bc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -41,6 +41,7 @@ import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonObjectRequest; +import com.keylesspalace.tusky.entity.Account; import org.json.JSONException; import org.json.JSONObject; @@ -49,6 +50,9 @@ import java.util.HashMap; import java.util.Map; import java.util.Stack; +import retrofit2.Call; +import retrofit2.Callback; + public class MainActivity extends BaseActivity { private static final String TAG = "MainActivity"; // logging tag and Volley request tag @@ -148,47 +152,23 @@ public class MainActivity extends BaseActivity { private void fetchUserInfo() { SharedPreferences preferences = getSharedPreferences( getString(R.string.preferences_file_key), Context.MODE_PRIVATE); - String domain = preferences.getString("domain", null); - final String accessToken = preferences.getString("accessToken", null); String id = preferences.getString("loggedInAccountId", null); String username = preferences.getString("loggedInAccountUsername", null); if (id != null && username != null) { loggedInAccountId = id; loggedInAccountUsername = username; } else { - String endpoint = getString(R.string.endpoint_verify_credentials); - String url = "https://" + domain + endpoint; - JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, null, - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - String username; - String id; - try { - id = response.getString("id"); - username = response.getString("acct"); - } catch (JSONException e) { - onFetchUserInfoFailure(e); - return; - } - onFetchUserInfoSuccess(id, username); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onFetchUserInfoFailure(error); - } - }) { + mastodonAPI.accountVerifyCredentials().enqueue(new Callback() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call call, retrofit2.Response response) { + onFetchUserInfoSuccess(response.body().id, response.body().username); } - }; - request.setTag(TAG); - VolleySingleton.getInstance(this).addToRequestQueue(request); + + @Override + public void onFailure(Call call, Throwable t) { + onFetchUserInfoFailure((Exception) t); + } + }); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java index 9bcf7057..66019ad1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java +++ b/app/src/main/java/com/keylesspalace/tusky/MastodonAPI.java @@ -9,6 +9,7 @@ import com.keylesspalace.tusky.entity.StatusContext; import java.util.List; +import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.ResponseBody; import retrofit2.Call; @@ -54,7 +55,7 @@ public interface MastodonAPI { @Multipart @POST("api/v1/media") - Call uploadMedia(@Part("file") RequestBody file); + Call uploadMedia(@Part("file") MultipartBody.Part file); @FormUrlEncoded @POST("api/v1/statuses") @@ -162,4 +163,8 @@ public interface MastodonAPI { Call authorizeFollowRequest(@Path("id") String accountId); @POST("api/v1/follow_requests/{id}/reject") Call rejectFollowRequest(@Path("id") String accountId); + + @FormUrlEncoded + @POST("api/v1/reports") + Call report(@Field("account_id") String accountId, @Field("status_ids[]") List statusIds, @Field("comment") String comment); } diff --git a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java b/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java index 34aa8694..3c13570a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ReportActivity.java @@ -45,10 +45,12 @@ import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Callback; @@ -145,45 +147,22 @@ public class ReportActivity extends BaseActivity { private void sendReport(final String accountId, final String[] statusIds, final String comment) { - JSONObject parameters = new JSONObject(); - try { - parameters.put("account_id", accountId); - parameters.put("status_ids", makeStringArrayCompat(statusIds)); - parameters.put("comment", comment); - } catch (JSONException e) { - Log.e(TAG, "Not all the report parameters have been properly set. " + e.getMessage()); - onSendFailure(accountId, statusIds, comment); - return; - } - String endpoint = getString(R.string.endpoint_reports); - String url = "https://" + domain + endpoint; - JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, parameters, - new Response.Listener() { - @Override - public void onResponse(JSONObject response) { - onSendSuccess(); - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError error) { - onSendFailure(accountId, statusIds, comment); - } - }) { + mastodonAPI.report(accountId, Arrays.asList(statusIds), comment).enqueue(new Callback() { @Override - public Map getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; + public void onResponse(Call call, retrofit2.Response response) { + onSendSuccess(); } - }; - request.setTag(TAG); - VolleySingleton.getInstance(this).addToRequestQueue(request); + + @Override + public void onFailure(Call call, Throwable t) { + onSendFailure(accountId, statusIds, comment); + } + }); } private void onSendSuccess() { - Toast.makeText(this, getString(R.string.confirmation_reported), Toast.LENGTH_SHORT) - .show(); + Snackbar bar = Snackbar.make(anyView, getString(R.string.confirmation_reported), Snackbar.LENGTH_SHORT); + bar.show(); finish(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/SFragment.java index 7d4feda3..e95d04cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/SFragment.java @@ -33,6 +33,7 @@ import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonObjectRequest; +import com.keylesspalace.tusky.entity.Relationship; import com.keylesspalace.tusky.entity.Status; import org.json.JSONObject; @@ -42,6 +43,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Callback; @@ -144,15 +146,45 @@ public class SFragment extends Fragment { } protected void follow(String id) { - api.followAccount(id).enqueue(null); + api.followAccount(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, retrofit2.Response response) { + + } + + @Override + public void onFailure(Call call, Throwable t) { + + } + }); } private void block(String id) { - api.blockAccount(id).enqueue(null); + api.blockAccount(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, retrofit2.Response response) { + + } + + @Override + public void onFailure(Call call, Throwable t) { + + } + }); } private void delete(String id) { - api.deleteStatus(id).enqueue(null); + api.deleteStatus(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, retrofit2.Response response) { + + } + + @Override + public void onFailure(Call call, Throwable t) { + + } + }); } protected void more(Status status, View view, final AdapterItemRemover adapter, diff --git a/app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java b/app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java index 1ef4db1c..d1e2adda 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/SpannedTypeAdapter.java @@ -2,6 +2,7 @@ package com.keylesspalace.tusky; import android.text.Spanned; +import com.emojione.Emojione; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; @@ -12,6 +13,6 @@ import java.lang.reflect.Type; class SpannedTypeAdapter implements JsonDeserializer { @Override public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return HtmlUtils.fromHtml(json.getAsString()); + return HtmlUtils.fromHtml(Emojione.shortnameToUnicode(json.getAsString(), false)); } } From 636c2f266e4aa5a5c98404f920b268719def3eea Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 17:03:13 +0100 Subject: [PATCH 09/15] Dismiss notifications when notifications fragment is opened --- .../com/keylesspalace/tusky/NotificationsFragment.java | 9 +++++++++ .../com/keylesspalace/tusky/PullNotificationService.java | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java index e6350e4b..5e0f3f20 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java @@ -15,6 +15,7 @@ package com.keylesspalace.tusky; +import android.app.NotificationManager; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Bundle; @@ -70,6 +71,14 @@ public class NotificationsFragment extends SFragment implements super.onDestroy(); } + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + NotificationManager notificationManager = + (NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(PullNotificationService.NOTIFY_ID); + super.onCreate(savedInstanceState); + } + @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, diff --git a/app/src/main/java/com/keylesspalace/tusky/PullNotificationService.java b/app/src/main/java/com/keylesspalace/tusky/PullNotificationService.java index 292cc2cd..f3be9f6d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/PullNotificationService.java +++ b/app/src/main/java/com/keylesspalace/tusky/PullNotificationService.java @@ -57,7 +57,7 @@ import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class PullNotificationService extends IntentService { - private static final int NOTIFY_ID = 6; // This is an arbitrary number. + static final int NOTIFY_ID = 6; // This is an arbitrary number. private static final String TAG = "PullNotifications"; // logging tag and Volley request tag public PullNotificationService() { From 9e82b64423fa935081a4035b1f6513cf342ace67 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 17:37:24 +0100 Subject: [PATCH 10/15] Fix #4 - Toot URLs can be shared --- .../java/com/keylesspalace/tusky/SFragment.java | 9 +++++++-- .../com/keylesspalace/tusky/entity/Account.java | 2 ++ .../com/keylesspalace/tusky/entity/Status.java | 14 ++++++++++++++ app/src/main/res/menu/status_more.xml | 6 +++--- app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/SFragment.java index e95d04cf..15ac7506 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/SFragment.java @@ -193,6 +193,7 @@ public class SFragment extends Fragment { final String accountId = status.getActionableStatus().account.id; final String accountUsename = status.getActionableStatus().account.username; final Spanned content = status.getActionableStatus().content; + final String statusUrl = status.getActionableStatus().url; PopupMenu popup = new PopupMenu(getContext(), view); // Give a different menu depending on whether this is the user's own toot or not. if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) { @@ -205,8 +206,12 @@ public class SFragment extends Fragment { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { - case R.id.status_follow: { - follow(accountId); + case R.id.status_share: { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_to))); return true; } case R.id.status_block: { diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.java b/app/src/main/java/com/keylesspalace/tusky/entity/Account.java index 6f03b030..d091512e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.java @@ -36,6 +36,8 @@ public class Account { public String header; + public boolean locked; + @SerializedName("followers_count") public String followersCount; diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java index 72c6682e..39378ee9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java @@ -24,6 +24,20 @@ import java.util.Date; public class Status { private Status actionableStatus; + public String url; + + @SerializedName("reblogs_count") + public String reblogsCount; + + @SerializedName("favourites_count") + public String favouritesCount; + + @SerializedName("in_reply_to_id") + public String inReplyToId; + + @SerializedName("in_reply_to_account_id") + public String inReplyToAccountId; + public String getActionableId() { return reblog == null ? id : reblog.id; } diff --git a/app/src/main/res/menu/status_more.xml b/app/src/main/res/menu/status_more.xml index 6886ea86..33617c9d 100644 --- a/app/src/main/res/menu/status_more.xml +++ b/app/src/main/res/menu/status_more.xml @@ -1,8 +1,8 @@ - - + Add media Privacy options Welcome back! + Share + Share toot URL to... From 02ccaf4610c47976c89223f8514ced863e44b34e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 17:51:44 +0100 Subject: [PATCH 11/15] Muting/unmuting accounts --- .../keylesspalace/tusky/AccountActivity.java | 69 +++++++++++++++++-- app/src/main/res/menu/account_toolbar.xml | 4 ++ app/src/main/res/values/strings.xml | 6 +- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index a4b30f62..14e6834e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -49,15 +49,15 @@ import java.util.List; import retrofit2.Call; import retrofit2.Callback; +import retrofit2.Response; public class AccountActivity extends BaseActivity { private static final String TAG = "AccountActivity"; // Volley request tag and logging tag - private String domain; - private String accessToken; private String accountId; private boolean following = false; private boolean blocking = false; + private boolean muting = false; private boolean isSelf; private String openInWebUrl; private TabLayout tabLayout; @@ -72,8 +72,6 @@ public class AccountActivity extends BaseActivity { SharedPreferences preferences = getSharedPreferences( getString(R.string.preferences_file_key), Context.MODE_PRIVATE); - domain = preferences.getString("domain", null); - accessToken = preferences.getString("accessToken", null); String loggedInAccountId = preferences.getString("loggedInAccountId", null); final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); @@ -244,7 +242,7 @@ public class AccountActivity extends BaseActivity { @Override public void onResponse(Call> call, retrofit2.Response> response) { Relationship relationship = response.body().get(0); - onObtainRelationshipsSuccess(relationship.following, relationship.blocking); + onObtainRelationshipsSuccess(relationship.following, relationship.blocking, relationship.muting); } @Override @@ -254,11 +252,12 @@ public class AccountActivity extends BaseActivity { }); } - private void onObtainRelationshipsSuccess(boolean following, boolean blocking) { + private void onObtainRelationshipsSuccess(boolean following, boolean blocking, boolean muting) { this.following = following; this.blocking = blocking; + this.muting = muting; - if (!following || !blocking) { + if (!following || !blocking || !muting) { invalidateOptionsMenu(); } @@ -310,10 +309,18 @@ public class AccountActivity extends BaseActivity { title = getString(R.string.action_block); } block.setTitle(title); + MenuItem mute = menu.findItem(R.id.action_mute); + if (muting) { + title = getString(R.string.action_unmute); + } else { + title = getString(R.string.action_mute); + } + mute.setTitle(title); } else { // It shouldn't be possible to block or follow yourself. menu.removeItem(R.id.action_follow); menu.removeItem(R.id.action_block); + menu.removeItem(R.id.action_mute); } return super.onPrepareOptionsMenu(menu); } @@ -396,6 +403,50 @@ public class AccountActivity extends BaseActivity { .show(); } + + private void mute(final String id) { + Callback cb = new Callback() { + @Override + public void onResponse(Call call, Response response) { + muting = response.body().muting; + updateButtons(); + } + + @Override + public void onFailure(Call call, Throwable t) { + onMuteFailure(id); + } + }; + + if (muting) { + mastodonAPI.unmuteAccount(id).enqueue(cb); + } else { + mastodonAPI.muteAccount(id).enqueue(cb); + } + } + + private void onMuteFailure(final String id) { + int messageId; + + if (muting) { + messageId = R.string.error_unmuting; + } else { + messageId = R.string.error_muting; + } + + View.OnClickListener listener = new View.OnClickListener() { + @Override + public void onClick(View v) { + mute(id); + } + }; + + Snackbar.make(findViewById(R.id.activity_account), messageId, Snackbar.LENGTH_LONG) + .setAction(R.string.action_retry, listener) + .show(); + } + + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -417,6 +468,10 @@ public class AccountActivity extends BaseActivity { block(accountId); return true; } + case R.id.action_mute: { + mute(accountId); + return true; + } } return super.onOptionsItemSelected(item); } diff --git a/app/src/main/res/menu/account_toolbar.xml b/app/src/main/res/menu/account_toolbar.xml index 3188882e..64b2f22f 100644 --- a/app/src/main/res/menu/account_toolbar.xml +++ b/app/src/main/res/menu/account_toolbar.xml @@ -10,6 +10,10 @@ android:title="@string/action_follow" app:showAsAction="never" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fbbb2218..20c9d4d8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -114,7 +114,7 @@ Preferences Favourites Blocked Users - Open In Web + Open in browser Set Toot! @@ -158,5 +158,9 @@ Welcome back! Share Share toot URL to... + Mute + Unmute + That user wasn\'t unmuted. + That user wasn\'t muted. From 9172636127d3161076ba9ce766d50a8955828ffb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 18:09:32 +0100 Subject: [PATCH 12/15] Add back buttons to favourites/blocks activities --- .../com/keylesspalace/tusky/BlocksActivity.java | 14 ++++++++++++++ .../keylesspalace/tusky/FavouritesActivity.java | 14 ++++++++++++++ app/src/main/res/values/strings.xml | 10 +++++----- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java b/app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java index 99e39193..534e5ea6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java @@ -21,6 +21,7 @@ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; +import android.view.MenuItem; public class BlocksActivity extends BaseActivity { @Override @@ -33,6 +34,8 @@ public class BlocksActivity extends BaseActivity { ActionBar bar = getSupportActionBar(); if (bar != null) { bar.setTitle(getString(R.string.title_blocks)); + bar.setDisplayHomeAsUpEnabled(true); + bar.setDisplayShowHomeEnabled(true); } FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); @@ -40,4 +43,15 @@ public class BlocksActivity extends BaseActivity { fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.commit(); } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + onBackPressed(); + return true; + } + } + return super.onOptionsItemSelected(item); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java b/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java index 6b66b846..8241b950 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java @@ -21,6 +21,7 @@ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; +import android.view.MenuItem; public class FavouritesActivity extends BaseActivity { @Override @@ -33,6 +34,8 @@ public class FavouritesActivity extends BaseActivity { ActionBar bar = getSupportActionBar(); if (bar != null) { bar.setTitle(getString(R.string.title_favourites)); + bar.setDisplayHomeAsUpEnabled(true); + bar.setDisplayShowHomeEnabled(true); } FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); @@ -40,4 +43,15 @@ public class FavouritesActivity extends BaseActivity { fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.commit(); } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + onBackPressed(); + return true; + } + } + return super.onOptionsItemSelected(item); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 20c9d4d8..cfc9ffd3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -143,15 +143,15 @@ Mention from %s Notifications - Enable Pull Notifcations - check for notifications periodically - Check Interval - how often to pull + Enable pull notifcations + Check for notifications periodically + Check interval + How often to pull Notify with a sound Notify with vibration Notify with light Appearance - Use The Light Theme + Use the Light Theme Submit Add media Privacy options From 7fa01371d9fc62451d3425b8d6c7c2ed31d510f0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 19:31:15 +0100 Subject: [PATCH 13/15] Add drawer to main activity --- app/build.gradle | 3 + .../com/keylesspalace/tusky/MainActivity.java | 182 ++++++++++++------ .../tusky/PreferencesActivity.java | 2 +- app/src/main/res/drawable/ic_block_24dp.xml | 9 + .../main/res/drawable/ic_exit_to_app_24dp.xml | 9 + .../main/res/drawable/ic_settings_24dp.xml | 9 + app/src/main/res/menu/main_toolbar.xml | 31 --- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/values/styles.xml | 10 + 9 files changed, 168 insertions(+), 91 deletions(-) create mode 100644 app/src/main/res/drawable/ic_block_24dp.xml create mode 100644 app/src/main/res/drawable/ic_exit_to_app_24dp.xml create mode 100644 app/src/main/res/drawable/ic_settings_24dp.xml delete mode 100644 app/src/main/res/menu/main_toolbar.xml diff --git a/app/build.gradle b/app/build.gradle index 762e622d..161bee86 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,4 +37,7 @@ dependencies { compile 'com.mikhaellopez:circularfillableloaders:1.2.0' compile 'com.squareup.retrofit2:retrofit:2.2.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' + compile('com.mikepenz:materialdrawer:5.8.2@aar') { + transitive = true + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 37f8e7bc..72d4c2bd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Build; import android.os.SystemClock; import android.preference.PreferenceManager; @@ -35,6 +36,7 @@ import android.transition.TransitionInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.widget.ImageView; import com.android.volley.AuthFailureError; import com.android.volley.Request; @@ -42,6 +44,18 @@ import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonObjectRequest; import com.keylesspalace.tusky.entity.Account; +import com.mikepenz.materialdrawer.AccountHeader; +import com.mikepenz.materialdrawer.AccountHeaderBuilder; +import com.mikepenz.materialdrawer.Drawer; +import com.mikepenz.materialdrawer.DrawerBuilder; +import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; +import com.mikepenz.materialdrawer.model.ProfileDrawerItem; +import com.mikepenz.materialdrawer.model.SecondaryDrawerItem; +import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; +import com.mikepenz.materialdrawer.model.interfaces.IProfile; +import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader; +import com.mikepenz.materialdrawer.util.DrawerImageLoader; +import com.squareup.picasso.Picasso; import org.json.JSONException; import org.json.JSONObject; @@ -63,6 +77,8 @@ public class MainActivity extends BaseActivity { private String loggedInAccountUsername; Stack pageHistory = new Stack(); private ViewPager viewPager; + private AccountHeader headerResult; + private Drawer drawer; @Override protected void onCreate(Bundle savedInstanceState) { @@ -84,6 +100,88 @@ public class MainActivity extends BaseActivity { } }); + headerResult = new AccountHeaderBuilder() + .withActivity(this) + .withSelectionListEnabledForSingleProfile(false) + .withTranslucentStatusBar(true) + .withCompactStyle(true) + .withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() { + @Override + public boolean onProfileImageClick(View view, IProfile profile, boolean current) { + Intent intent = new Intent(MainActivity.this, AccountActivity.class); + intent.putExtra("id", loggedInAccountId); + startActivity(intent); + return false; + } + + @Override + public boolean onProfileImageLongClick(View view, IProfile profile, boolean current) { + return false; + } + }) + .build(); + + DrawerImageLoader.init(new AbstractDrawerImageLoader() { + @Override + public void set(ImageView imageView, Uri uri, Drawable placeholder) { + Picasso.with(imageView.getContext()).load(uri).placeholder(placeholder).into(imageView); + } + + @Override + public void cancel(ImageView imageView) { + Picasso.with(imageView.getContext()).cancelRequest(imageView); + } + }); + + drawer = new DrawerBuilder() + .withActivity(this) + .withToolbar(toolbar) + .withTranslucentStatusBar(true) + .withAccountHeader(headerResult) + .withHasStableIds(true) + .withSelectedItem(-1) + .addDrawerItems( + new PrimaryDrawerItem().withIdentifier(1).withName(getString(R.string.action_view_favourites)).withIcon(R.drawable.ic_star_24dp).withSelectable(false), + new PrimaryDrawerItem().withIdentifier(2).withName(getString(R.string.action_view_blocks)).withIcon(R.drawable.ic_block_24dp).withSelectable(false), + new PrimaryDrawerItem().withIdentifier(3).withName(getString(R.string.action_view_preferences)).withIcon(R.drawable.ic_settings_24dp).withSelectable(false), + new PrimaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_logout)).withIcon(R.drawable.ic_exit_to_app_24dp).withSelectable(false) + ) + .withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() { + @Override + public boolean onItemClick(View view, int position, IDrawerItem drawerItem) { + if (drawerItem != null) { + long drawerItemIdentifier = drawerItem.getIdentifier(); + + if (drawerItemIdentifier == 1) { + Intent intent = new Intent(MainActivity.this, FavouritesActivity.class); + startActivity(intent); + } else if (drawerItemIdentifier == 2) { + Intent intent = new Intent(MainActivity.this, BlocksActivity.class); + startActivity(intent); + } else if (drawerItemIdentifier == 3) { + Intent intent = new Intent(MainActivity.this, PreferencesActivity.class); + startActivity(intent); + } else if (drawerItemIdentifier == 4) { + if (notificationServiceEnabled) { + alarmManager.cancel(serviceAlarmIntent); + } + SharedPreferences preferences = getSharedPreferences( + getString(R.string.preferences_file_key), Context.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.remove("domain"); + editor.remove("accessToken"); + editor.apply(); + Intent intent = new Intent(MainActivity.this, SplashActivity.class); + startActivity(intent); + finish(); + } + } + + return false; + } + }) + .build(); + // Setup the tabs and timeline pager. TimelinePagerAdapter adapter = new TimelinePagerAdapter(getSupportFragmentManager()); String[] pageTitles = { @@ -152,16 +250,34 @@ public class MainActivity extends BaseActivity { private void fetchUserInfo() { SharedPreferences preferences = getSharedPreferences( getString(R.string.preferences_file_key), Context.MODE_PRIVATE); + final String domain = preferences.getString("domain", null); String id = preferences.getString("loggedInAccountId", null); String username = preferences.getString("loggedInAccountUsername", null); - if (id != null && username != null) { - loggedInAccountId = id; - loggedInAccountUsername = username; - } else { + //if (id != null && username != null) { + // loggedInAccountId = id; + // loggedInAccountUsername = username; + //} else { mastodonAPI.accountVerifyCredentials().enqueue(new Callback() { @Override public void onResponse(Call call, retrofit2.Response response) { - onFetchUserInfoSuccess(response.body().id, response.body().username); + Account me = response.body(); + ImageView background = headerResult.getHeaderBackgroundView(); + + Picasso.with(MainActivity.this) + .load(me.header) + .placeholder(R.drawable.account_header_missing) + .resize(background.getWidth(), background.getHeight()) + .centerCrop() + .into(background); + + headerResult.addProfiles( + new ProfileDrawerItem() + .withName(me.displayName) + .withEmail(String.format("%s@%s", me.username, domain)) + .withIcon(me.avatar) + ); + + //onFetchUserInfoSuccess(response.body().id, response.body().username); } @Override @@ -169,7 +285,7 @@ public class MainActivity extends BaseActivity { onFetchUserInfoFailure((Exception) t); } }); - } + //} } private void onFetchUserInfoSuccess(String id, String username) { @@ -187,59 +303,11 @@ public class MainActivity extends BaseActivity { Log.e(TAG, "Failed to fetch user info. " + exception.getMessage()); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.main_toolbar, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_view_profile: { - Intent intent = new Intent(this, AccountActivity.class); - intent.putExtra("id", loggedInAccountId); - startActivity(intent); - return true; - } - case R.id.action_view_preferences: { - Intent intent = new Intent(this, PreferencesActivity.class); - startActivity(intent); - return true; - } - case R.id.action_view_favourites: { - Intent intent = new Intent(this, FavouritesActivity.class); - startActivity(intent); - return true; - } - case R.id.action_view_blocks: { - Intent intent = new Intent(this, BlocksActivity.class); - startActivity(intent); - return true; - } - case R.id.action_logout: { - if (notificationServiceEnabled) { - alarmManager.cancel(serviceAlarmIntent); - } - SharedPreferences preferences = getSharedPreferences( - getString(R.string.preferences_file_key), Context.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.remove("domain"); - editor.remove("accessToken"); - editor.apply(); - Intent intent = new Intent(this, SplashActivity.class); - startActivity(intent); - finish(); - return true; - } - } - - return super.onOptionsItemSelected(item); - } - @Override public void onBackPressed() { - if(pageHistory.empty()) { + if(drawer != null && drawer.isDrawerOpen()) { + drawer.closeDrawer(); + } else if(pageHistory.empty()) { super.onBackPressed(); } else { pageHistory.pop(); diff --git a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java index 53b29cea..ffc2811a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/PreferencesActivity.java @@ -22,7 +22,7 @@ import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; -public class PreferencesActivity extends AppCompatActivity +public class PreferencesActivity extends BaseActivity implements SharedPreferences.OnSharedPreferenceChangeListener { private boolean themeSwitched; diff --git a/app/src/main/res/drawable/ic_block_24dp.xml b/app/src/main/res/drawable/ic_block_24dp.xml new file mode 100644 index 00000000..7a184d9d --- /dev/null +++ b/app/src/main/res/drawable/ic_block_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_exit_to_app_24dp.xml b/app/src/main/res/drawable/ic_exit_to_app_24dp.xml new file mode 100644 index 00000000..93378231 --- /dev/null +++ b/app/src/main/res/drawable/ic_exit_to_app_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_24dp.xml b/app/src/main/res/drawable/ic_settings_24dp.xml new file mode 100644 index 00000000..4c81fb46 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu/main_toolbar.xml b/app/src/main/res/menu/main_toolbar.xml deleted file mode 100644 index 2517ee5d..00000000 --- a/app/src/main/res/menu/main_toolbar.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cfc9ffd3..c48f4a5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,7 +91,7 @@ %s followed you Reporting @%s - Additional Comments? + Additional comments? Compose Login with Mastodon @@ -113,7 +113,7 @@ Profile Preferences Favourites - Blocked Users + Blocked users Open in browser Set diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index df0430db..8546465d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -55,6 +55,16 @@ @color/notification_icon_tint_dark @color/report_status_background_dark @drawable/report_status_divider_dark + + @color/color_primary_dark + @color/text_color_primary_dark + @color/toolbar_icon_dark + @color/text_color_secondary_dark + @color/text_color_tertiary_dark + @color/color_primary_dark_dark + @color/window_background_dark + @color/text_color_primary_dark + @color/text_color_primary_dark