From 750c1c80a0af3bb3f4d10b78e801d87e12264cb5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Mar 2017 00:27:37 +0100 Subject: [PATCH] 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 f517d4021..3f5c690aa 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 358875160..23391fd90 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 7568dccd6..fa529ed98 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 375731de1..47b3283f3 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 846ad3e33..2cef310e0 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 f76911726..000000000 --- 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 c8306542a..5b8ea652e 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 0aeded4df..e6350e4bd 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 d562275a6..292cc2cd2 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 fb0a8ccdc..34aa86948 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 ba1fea6ec..58e23ce49 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 9ecb2d120..000000000 --- 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 abf0a5b6d..548bde062 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 09521551c..ac346b986 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 3e9ed57b3..4cc2dc482 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 792afd409..428413cf5 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 0f41e534e..907d6c906 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 9389c0bfd..f615eb048 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 000000000..fb0b7b9a4 --- /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 000000000..72c6682e3 --- /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 4099df6b5..338b55037 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 {