Statuses and notifications loaded/parsed via Retrofit/GSON
Notification checker uses since_id as the more exact check-for-updates
This commit is contained in:
parent
3120fbed4c
commit
750c1c80a0
21 changed files with 418 additions and 777 deletions
|
@ -75,7 +75,6 @@ public class AccountActivity extends BaseActivity {
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_account);
|
setContentView(R.layout.activity_account);
|
||||||
createMastodonAPI();
|
|
||||||
|
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
accountId = intent.getStringExtra("id");
|
accountId = intent.getStringExtra("id");
|
||||||
|
|
|
@ -184,21 +184,17 @@ public class AccountFragment extends Fragment implements AccountActionListener,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
String endpoint;
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
default:
|
default:
|
||||||
case FOLLOWS: {
|
case FOLLOWS: {
|
||||||
endpoint = String.format(getString(R.string.endpoint_following), accountId);
|
|
||||||
api.accountFollowing(accountId, fromId, null, null).enqueue(cb);
|
api.accountFollowing(accountId, fromId, null, null).enqueue(cb);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FOLLOWERS: {
|
case FOLLOWERS: {
|
||||||
endpoint = String.format(getString(R.string.endpoint_followers), accountId);
|
|
||||||
api.accountFollowers(accountId, fromId, null, null).enqueue(cb);
|
api.accountFollowers(accountId, fromId, null, null).enqueue(cb);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case BLOCKS: {
|
case BLOCKS: {
|
||||||
endpoint = getString(R.string.endpoint_blocks);
|
|
||||||
api.blocks(fromId, null, null).enqueue(cb);
|
api.blocks(fromId, null, null).enqueue(cb);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,9 @@ public class BaseActivity extends AppCompatActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
createMastodonAPI();
|
||||||
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
|
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
|
||||||
setTheme(R.style.AppTheme_Light);
|
setTheme(R.style.AppTheme_Light);
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ import com.android.volley.Request;
|
||||||
import com.android.volley.Response;
|
import com.android.volley.Response;
|
||||||
import com.android.volley.VolleyError;
|
import com.android.volley.VolleyError;
|
||||||
import com.android.volley.toolbox.JsonObjectRequest;
|
import com.android.volley.toolbox.JsonObjectRequest;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
|
@ -2,7 +2,9 @@ package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
import com.keylesspalace.tusky.entity.Media;
|
import com.keylesspalace.tusky.entity.Media;
|
||||||
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Relationship;
|
import com.keylesspalace.tusky.entity.Relationship;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.entity.StatusContext;
|
import com.keylesspalace.tusky.entity.StatusContext;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -146,7 +148,7 @@ public interface MastodonAPI {
|
||||||
@Query("limit") Integer limit);
|
@Query("limit") Integer limit);
|
||||||
|
|
||||||
@GET("api/v1/favourites")
|
@GET("api/v1/favourites")
|
||||||
Call<List<Account>> favourites(
|
Call<List<Status>> favourites(
|
||||||
@Query("max_id") String maxId,
|
@Query("max_id") String maxId,
|
||||||
@Query("since_id") String sinceId,
|
@Query("since_id") String sinceId,
|
||||||
@Query("limit") Integer limit);
|
@Query("limit") Integer limit);
|
||||||
|
|
|
@ -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
|
|
||||||
* <http://www.gnu.org/licenses/>. */
|
|
||||||
|
|
||||||
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<Notification> parse(JSONArray array) throws JSONException {
|
|
||||||
List<Notification> 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -28,6 +28,8 @@ import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -86,26 +88,26 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
if (position < notifications.size()) {
|
if (position < notifications.size()) {
|
||||||
Notification notification = notifications.get(position);
|
Notification notification = notifications.get(position);
|
||||||
Notification.Type type = notification.getType();
|
Notification.Type type = notification.type;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MENTION: {
|
case MENTION: {
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||||
Status status = notification.getStatus();
|
Status status = notification.status;
|
||||||
holder.setupWithStatus(status, statusListener);
|
holder.setupWithStatus(status, statusListener);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FAVOURITE:
|
case FAVOURITE:
|
||||||
case REBLOG: {
|
case REBLOG: {
|
||||||
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
||||||
holder.setMessage(type, notification.getDisplayName(),
|
holder.setMessage(type, notification.account.displayName,
|
||||||
notification.getStatus());
|
notification.status);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FOLLOW: {
|
case FOLLOW: {
|
||||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
||||||
holder.setMessage(notification.getDisplayName(), notification.getUsername(),
|
holder.setMessage(notification.account.displayName, notification.account.username,
|
||||||
notification.getAvatar());
|
notification.account.avatar);
|
||||||
holder.setupButtons(followListener, notification.getAccountId());
|
holder.setupButtons(followListener, notification.account.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,7 +128,7 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
|
||||||
return VIEW_TYPE_FOOTER;
|
return VIEW_TYPE_FOOTER;
|
||||||
} else {
|
} else {
|
||||||
Notification notification = notifications.get(position);
|
Notification notification = notifications.get(position);
|
||||||
switch (notification.getType()) {
|
switch (notification.type) {
|
||||||
default:
|
default:
|
||||||
case MENTION: {
|
case MENTION: {
|
||||||
return VIEW_TYPE_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(),
|
str.setSpan(new android.text.style.StyleSpan(Typeface.BOLD), 0, displayName.length(),
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
message.setText(str);
|
message.setText(str);
|
||||||
statusContent.setText(status.getContent());
|
statusContent.setText(status.content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ import com.android.volley.AuthFailureError;
|
||||||
import com.android.volley.Response;
|
import com.android.volley.Response;
|
||||||
import com.android.volley.VolleyError;
|
import com.android.volley.VolleyError;
|
||||||
import com.android.volley.toolbox.JsonArrayRequest;
|
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.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
@ -40,6 +42,9 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
|
||||||
public class NotificationsFragment extends SFragment implements
|
public class NotificationsFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener,
|
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener,
|
||||||
NotificationsAdapter.FollowListener {
|
NotificationsAdapter.FollowListener {
|
||||||
|
@ -92,7 +97,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
||||||
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
||||||
if (notification != null) {
|
if (notification != null) {
|
||||||
sendFetchNotificationsRequest(notification.getId());
|
sendFetchNotificationsRequest(notification.id);
|
||||||
} else {
|
} else {
|
||||||
sendFetchNotificationsRequest();
|
sendFetchNotificationsRequest();
|
||||||
}
|
}
|
||||||
|
@ -135,37 +140,19 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendFetchNotificationsRequest(final String fromId) {
|
private void sendFetchNotificationsRequest(final String fromId) {
|
||||||
String endpoint = getString(R.string.endpoint_notifications);
|
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
|
||||||
String url = "https://" + domain + endpoint;
|
|
||||||
if (fromId != null) {
|
api.notifications(fromId, null, null).enqueue(new Callback<List<Notification>>() {
|
||||||
url += "?max_id=" + fromId;
|
|
||||||
}
|
|
||||||
JsonArrayRequest request = new JsonArrayRequest(url,
|
|
||||||
new Response.Listener<JSONArray>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(JSONArray response) {
|
|
||||||
try {
|
|
||||||
List<Notification> notifications = Notification.parse(response);
|
|
||||||
onFetchNotificationsSuccess(notifications, fromId);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
onFetchNotificationsFailure(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, new Response.ErrorListener() {
|
|
||||||
@Override
|
|
||||||
public void onErrorResponse(VolleyError error) {
|
|
||||||
onFetchNotificationsFailure(error);
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
public void onResponse(Call<List<Notification>> call, retrofit2.Response<List<Notification>> response) {
|
||||||
Map<String, String> headers = new HashMap<>();
|
onFetchNotificationsSuccess(response.body(), fromId);
|
||||||
headers.put("Authorization", "Bearer " + accessToken);
|
|
||||||
return headers;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
request.setTag(TAG);
|
@Override
|
||||||
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
|
public void onFailure(Call<List<Notification>> call, Throwable t) {
|
||||||
|
onFetchNotificationsFailure((Exception) t);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendFetchNotificationsRequest() {
|
private void sendFetchNotificationsRequest() {
|
||||||
|
@ -174,7 +161,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
|
|
||||||
private static boolean findNotification(List<Notification> notifications, String id) {
|
private static boolean findNotification(List<Notification> notifications, String id) {
|
||||||
for (Notification notification : notifications) {
|
for (Notification notification : notifications) {
|
||||||
if (notification.getId().equals(id)) {
|
if (notification.id.equals(id)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,7 +205,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
public void onLoadMore() {
|
public void onLoadMore() {
|
||||||
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
||||||
if (notification != null) {
|
if (notification != null) {
|
||||||
sendFetchNotificationsRequest(notification.getId());
|
sendFetchNotificationsRequest(notification.id);
|
||||||
} else {
|
} else {
|
||||||
sendFetchNotificationsRequest();
|
sendFetchNotificationsRequest();
|
||||||
}
|
}
|
||||||
|
@ -226,22 +213,22 @@ public class NotificationsFragment extends SFragment implements
|
||||||
|
|
||||||
public void onReply(int position) {
|
public void onReply(int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = adapter.getItem(position);
|
||||||
super.reply(notification.getStatus());
|
super.reply(notification.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onReblog(boolean reblog, int position) {
|
public void onReblog(boolean reblog, int position) {
|
||||||
Notification notification = adapter.getItem(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) {
|
public void onFavourite(boolean favourite, int position) {
|
||||||
Notification notification = adapter.getItem(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) {
|
public void onMore(View view, int position) {
|
||||||
Notification notification = adapter.getItem(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) {
|
public void onViewMedia(String url, Status.MediaAttachment.Type type) {
|
||||||
|
@ -250,7 +237,7 @@ public class NotificationsFragment extends SFragment implements
|
||||||
|
|
||||||
public void onViewThread(int position) {
|
public void onViewThread(int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = adapter.getItem(position);
|
||||||
super.viewThread(notification.getStatus());
|
super.viewThread(notification.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onViewTag(String tag) {
|
public void onViewTag(String tag) {
|
||||||
|
|
|
@ -26,22 +26,36 @@ import android.provider.Settings;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.NotificationCompat;
|
import android.support.v4.app.NotificationCompat;
|
||||||
import android.support.v4.app.TaskStackBuilder;
|
import android.support.v4.app.TaskStackBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
|
||||||
import com.android.volley.AuthFailureError;
|
import com.android.volley.AuthFailureError;
|
||||||
import com.android.volley.Response;
|
import com.android.volley.Response;
|
||||||
import com.android.volley.VolleyError;
|
import com.android.volley.VolleyError;
|
||||||
import com.android.volley.toolbox.ImageRequest;
|
import com.android.volley.toolbox.ImageRequest;
|
||||||
import com.android.volley.toolbox.JsonArrayRequest;
|
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.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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 {
|
public class PullNotificationService extends IntentService {
|
||||||
private static final int NOTIFY_ID = 6; // This is an arbitrary number.
|
private static final int NOTIFY_ID = 6; // This is an arbitrary number.
|
||||||
private static final String TAG = "PullNotifications"; // logging tag and Volley request tag
|
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);
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||||
String domain = preferences.getString("domain", null);
|
String domain = preferences.getString("domain", null);
|
||||||
String accessToken = preferences.getString("accessToken", null);
|
String accessToken = preferences.getString("accessToken", null);
|
||||||
long date = preferences.getLong("lastUpdate", 0);
|
String lastUpdateId = preferences.getString("lastUpdateId", null);
|
||||||
Date lastUpdate = null;
|
checkNotifications(domain, accessToken, lastUpdateId);
|
||||||
if (date != 0) {
|
|
||||||
lastUpdate = new Date(date);
|
|
||||||
}
|
|
||||||
checkNotifications(domain, accessToken, lastUpdate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkNotifications(final String domain, final String accessToken,
|
private void checkNotifications(final String domain, final String accessToken,
|
||||||
final Date lastUpdate) {
|
final String lastUpdateId) {
|
||||||
String endpoint = getString(R.string.endpoint_notifications);
|
OkHttpClient okHttpClient = new OkHttpClient.Builder()
|
||||||
String url = "https://" + domain + endpoint;
|
.addInterceptor(new Interceptor() {
|
||||||
JsonArrayRequest request = new JsonArrayRequest(url,
|
|
||||||
new Response.Listener<JSONArray>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(JSONArray response) {
|
public okhttp3.Response intercept(Chain chain) throws IOException {
|
||||||
List<Notification> notifications;
|
Request originalRequest = chain.request();
|
||||||
try {
|
|
||||||
notifications = Notification.parse(response);
|
Request.Builder builder = originalRequest.newBuilder()
|
||||||
} catch (JSONException e) {
|
.header("Authorization", String.format("Bearer %s", accessToken));
|
||||||
onCheckNotificationsFailure(e);
|
|
||||||
return;
|
Request newRequest = builder.build();
|
||||||
}
|
|
||||||
onCheckNotificationsSuccess(notifications, lastUpdate);
|
return chain.proceed(newRequest);
|
||||||
}
|
}
|
||||||
}, new Response.ErrorListener() {
|
})
|
||||||
@Override
|
.build();
|
||||||
public void onErrorResponse(VolleyError error) {
|
|
||||||
onCheckNotificationsFailure(error);
|
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<List<Notification>>() {
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
public void onResponse(Call<List<Notification>> call, retrofit2.Response<List<Notification>> response) {
|
||||||
Map<String, String> headers = new HashMap<>();
|
onCheckNotificationsSuccess(response.body(), lastUpdateId);
|
||||||
headers.put("Authorization", "Bearer " + accessToken);
|
|
||||||
return headers;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
request.setTag(TAG);
|
@Override
|
||||||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
public void onFailure(Call<List<Notification>> call, Throwable t) {
|
||||||
|
onCheckNotificationsFailure((Exception) t);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onCheckNotificationsSuccess(List<Notification> notifications, Date lastUpdate) {
|
private void onCheckNotificationsSuccess(List<com.keylesspalace.tusky.entity.Notification> notifications, String lastUpdateId) {
|
||||||
Date newest = null;
|
|
||||||
List<MentionResult> mentions = new ArrayList<>();
|
List<MentionResult> mentions = new ArrayList<>();
|
||||||
for (Notification notification : notifications) {
|
|
||||||
if (notification.getType() == Notification.Type.MENTION) {
|
for (com.keylesspalace.tusky.entity.Notification notification : notifications) {
|
||||||
Status status = notification.getStatus();
|
if (notification.type == com.keylesspalace.tusky.entity.Notification.Type.MENTION) {
|
||||||
|
Status status = notification.status;
|
||||||
|
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
Date createdAt = status.getCreatedAt();
|
MentionResult mention = new MentionResult();
|
||||||
if (lastUpdate == null || createdAt.after(lastUpdate)) {
|
mention.content = status.content.toString();
|
||||||
MentionResult mention = new MentionResult();
|
mention.displayName = notification.account.displayName;
|
||||||
mention.content = status.getContent().toString();
|
mention.avatarUrl = status.account.avatar;
|
||||||
mention.displayName = notification.getDisplayName();
|
mentions.add(mention);
|
||||||
mention.avatarUrl = status.getAvatar();
|
|
||||||
mentions.add(mention);
|
|
||||||
}
|
|
||||||
if (newest == null || createdAt.after(newest)) {
|
|
||||||
newest = createdAt;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
long now = new Date().getTime();
|
|
||||||
if (mentions.size() > 0) {
|
if (notifications.size() > 0) {
|
||||||
SharedPreferences preferences = getSharedPreferences(
|
SharedPreferences preferences = getSharedPreferences(
|
||||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
editor.putLong("lastUpdate", now);
|
editor.putString("lastUpdateId", notifications.get(0).id);
|
||||||
editor.apply();
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentions.size() > 0) {
|
||||||
loadAvatar(mentions, mentions.get(0).avatarUrl);
|
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) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
notificationManager.notify(NOTIFY_ID, builder.build());
|
notificationManager.notify(NOTIFY_ID, builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dismissStaleNotifications() {
|
|
||||||
NotificationManager notificationManager =
|
|
||||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
notificationManager.cancel(NOTIFY_ID);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import com.android.volley.Response;
|
||||||
import com.android.volley.VolleyError;
|
import com.android.volley.VolleyError;
|
||||||
import com.android.volley.toolbox.JsonArrayRequest;
|
import com.android.volley.toolbox.JsonArrayRequest;
|
||||||
import com.android.volley.toolbox.JsonObjectRequest;
|
import com.android.volley.toolbox.JsonObjectRequest;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
@ -48,6 +49,9 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
|
||||||
public class ReportActivity extends BaseActivity {
|
public class ReportActivity extends BaseActivity {
|
||||||
private static final String TAG = "ReportActivity"; // logging tag and Volley request tag
|
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) {
|
private void fetchRecentStatuses(String accountId) {
|
||||||
String endpoint = String.format(getString(R.string.endpoint_statuses), accountId);
|
mastodonAPI.accountStatuses(accountId, null, null, null).enqueue(new Callback<List<Status>>() {
|
||||||
String url = "https://" + domain + endpoint;
|
|
||||||
JsonArrayRequest request = new JsonArrayRequest(url,
|
|
||||||
new Response.Listener<JSONArray>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(JSONArray response) {
|
|
||||||
List<Status> statusList;
|
|
||||||
try {
|
|
||||||
statusList = Status.parse(response);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
onFetchStatusesFailure(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Add all the statuses except reblogs.
|
|
||||||
List<ReportAdapter.ReportStatus> 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);
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
public void onResponse(Call<List<Status>> call, retrofit2.Response<List<Status>> response) {
|
||||||
Map<String, String> headers = new HashMap<>();
|
List<Status> statusList = response.body();
|
||||||
headers.put("Authorization", "Bearer " + accessToken);
|
List<ReportAdapter.ReportStatus> itemList = new ArrayList<>();
|
||||||
return headers;
|
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);
|
@Override
|
||||||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
public void onFailure(Call<List<Status>> call, Throwable t) {
|
||||||
|
onFetchStatusesFailure((Exception) t);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFetchStatusesFailure(Exception exception) {
|
private void onFetchStatusesFailure(Exception exception) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import com.android.volley.Request;
|
||||||
import com.android.volley.Response;
|
import com.android.volley.Response;
|
||||||
import com.android.volley.VolleyError;
|
import com.android.volley.VolleyError;
|
||||||
import com.android.volley.toolbox.JsonObjectRequest;
|
import com.android.volley.toolbox.JsonObjectRequest;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
@ -111,24 +112,24 @@ public class SFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void reply(Status status) {
|
protected void reply(Status status) {
|
||||||
String inReplyToId = status.getId();
|
String inReplyToId = status.getActionableId();
|
||||||
Status.Mention[] mentions = status.getMentions();
|
Status.Mention[] mentions = status.mentions;
|
||||||
List<String> mentionedUsernames = new ArrayList<>();
|
List<String> mentionedUsernames = new ArrayList<>();
|
||||||
for (Status.Mention mention : mentions) {
|
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);
|
mentionedUsernames.remove(loggedInUsername);
|
||||||
Intent intent = new Intent(getContext(), ComposeActivity.class);
|
Intent intent = new Intent(getContext(), ComposeActivity.class);
|
||||||
intent.putExtra("in_reply_to_id", inReplyToId);
|
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]));
|
intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0]));
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void reblog(final Status status, final boolean reblog,
|
protected void reblog(final Status status, final boolean reblog,
|
||||||
final RecyclerView.Adapter adapter, final int position) {
|
final RecyclerView.Adapter adapter, final int position) {
|
||||||
String id = status.getId();
|
String id = status.getActionableId();
|
||||||
String endpoint;
|
String endpoint;
|
||||||
if (reblog) {
|
if (reblog) {
|
||||||
endpoint = String.format(getString(R.string.endpoint_reblog), id);
|
endpoint = String.format(getString(R.string.endpoint_reblog), id);
|
||||||
|
@ -139,7 +140,7 @@ public class SFragment extends Fragment {
|
||||||
new Response.Listener<JSONObject>() {
|
new Response.Listener<JSONObject>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(JSONObject response) {
|
public void onResponse(JSONObject response) {
|
||||||
status.setReblogged(reblog);
|
status.reblogged = reblog;
|
||||||
adapter.notifyItemChanged(position);
|
adapter.notifyItemChanged(position);
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
|
@ -147,7 +148,7 @@ public class SFragment extends Fragment {
|
||||||
|
|
||||||
protected void favourite(final Status status, final boolean favourite,
|
protected void favourite(final Status status, final boolean favourite,
|
||||||
final RecyclerView.Adapter adapter, final int position) {
|
final RecyclerView.Adapter adapter, final int position) {
|
||||||
String id = status.getId();
|
String id = status.getActionableId();
|
||||||
String endpoint;
|
String endpoint;
|
||||||
if (favourite) {
|
if (favourite) {
|
||||||
endpoint = String.format(getString(R.string.endpoint_favourite), id);
|
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<JSONObject>() {
|
sendRequest(Request.Method.POST, endpoint, null, new Response.Listener<JSONObject>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(JSONObject response) {
|
public void onResponse(JSONObject response) {
|
||||||
status.setFavourited(favourite);
|
status.favourited = favourite;
|
||||||
adapter.notifyItemChanged(position);
|
adapter.notifyItemChanged(position);
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
|
@ -180,10 +181,10 @@ public class SFragment extends Fragment {
|
||||||
|
|
||||||
protected void more(Status status, View view, final AdapterItemRemover adapter,
|
protected void more(Status status, View view, final AdapterItemRemover adapter,
|
||||||
final int position) {
|
final int position) {
|
||||||
final String id = status.getId();
|
final String id = status.getActionableId();
|
||||||
final String accountId = status.getAccountId();
|
final String accountId = status.getActionableStatus().account.id;
|
||||||
final String accountUsename = status.getUsername();
|
final String accountUsename = status.getActionableStatus().account.username;
|
||||||
final Spanned content = status.getContent();
|
final Spanned content = status.getActionableStatus().content;
|
||||||
PopupMenu popup = new PopupMenu(getContext(), view);
|
PopupMenu popup = new PopupMenu(getContext(), view);
|
||||||
// Give a different menu depending on whether this is the user's own toot or not.
|
// Give a different menu depending on whether this is the user's own toot or not.
|
||||||
if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) {
|
if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) {
|
||||||
|
@ -234,12 +235,8 @@ public class SFragment extends Fragment {
|
||||||
protected void viewMedia(String url, Status.MediaAttachment.Type type) {
|
protected void viewMedia(String url, Status.MediaAttachment.Type type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case IMAGE: {
|
case IMAGE: {
|
||||||
Fragment newFragment;
|
Fragment newFragment = ViewMediaFragment.newInstance(url);
|
||||||
if (fileExtensionMatches(url, "gif")) {
|
|
||||||
newFragment = ViewGifFragment.newInstance(url);
|
|
||||||
} else {
|
|
||||||
newFragment = ViewMediaFragment.newInstance(url);
|
|
||||||
}
|
|
||||||
FragmentManager manager = getFragmentManager();
|
FragmentManager manager = getFragmentManager();
|
||||||
manager.beginTransaction()
|
manager.beginTransaction()
|
||||||
.add(R.id.overlay_fragment_container, newFragment)
|
.add(R.id.overlay_fragment_container, newFragment)
|
||||||
|
@ -264,7 +261,7 @@ public class SFragment extends Fragment {
|
||||||
|
|
||||||
protected void viewThread(Status status) {
|
protected void viewThread(Status status) {
|
||||||
Intent intent = new Intent(getContext(), ViewThreadActivity.class);
|
Intent intent = new Intent(getContext(), ViewThreadActivity.class);
|
||||||
intent.putExtra("id", status.getId());
|
intent.putExtra("id", status.id);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
* <http://www.gnu.org/licenses/>. */
|
|
||||||
|
|
||||||
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<Status> parse(JSONArray array) throws JSONException {
|
|
||||||
List<Status> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,6 +17,8 @@ package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
interface StatusActionListener {
|
interface StatusActionListener {
|
||||||
void onReply(int position);
|
void onReply(int position);
|
||||||
void onReblog(final boolean reblog, final int position);
|
void onReblog(final boolean reblog, final int position);
|
||||||
|
|
|
@ -30,6 +30,7 @@ import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
import com.varunest.sparkbutton.SparkButton;
|
import com.varunest.sparkbutton.SparkButton;
|
||||||
import com.varunest.sparkbutton.SparkEventListener;
|
import com.varunest.sparkbutton.SparkEventListener;
|
||||||
|
@ -124,8 +125,8 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
final String accountUsername = text.subSequence(1, text.length()).toString();
|
final String accountUsername = text.subSequence(1, text.length()).toString();
|
||||||
String id = null;
|
String id = null;
|
||||||
for (Status.Mention mention: mentions) {
|
for (Status.Mention mention: mentions) {
|
||||||
if (mention.getUsername().equals(accountUsername)) {
|
if (mention.username.equals(accountUsername)) {
|
||||||
id = mention.getId();
|
id = mention.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
|
@ -227,7 +228,7 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS);
|
final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS);
|
||||||
|
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
String previewUrl = attachments[i].getPreviewUrl();
|
String previewUrl = attachments[i].previewUrl;
|
||||||
|
|
||||||
previews[i].setVisibility(View.VISIBLE);
|
previews[i].setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
@ -236,8 +237,8 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
.placeholder(mediaPreviewUnloadedId)
|
.placeholder(mediaPreviewUnloadedId)
|
||||||
.into(previews[i]);
|
.into(previews[i]);
|
||||||
|
|
||||||
final String url = attachments[i].getUrl();
|
final String url = attachments[i].url;
|
||||||
final Status.MediaAttachment.Type type = attachments[i].getType();
|
final Status.MediaAttachment.Type type = attachments[i].type;
|
||||||
previews[i].setOnClickListener(new View.OnClickListener() {
|
previews[i].setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
@ -339,33 +340,35 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupWithStatus(Status status, StatusActionListener listener) {
|
void setupWithStatus(Status status, StatusActionListener listener) {
|
||||||
setDisplayName(status.getDisplayName());
|
Status realStatus = status.getActionableStatus();
|
||||||
setUsername(status.getUsername());
|
|
||||||
setCreatedAt(status.getCreatedAt());
|
setDisplayName(realStatus.account.displayName);
|
||||||
setContent(status.getContent(), status.getMentions(), listener);
|
setUsername(realStatus.account.username);
|
||||||
setAvatar(status.getAvatar());
|
setCreatedAt(realStatus.createdAt);
|
||||||
setReblogged(status.getReblogged());
|
setContent(realStatus.content, realStatus.mentions, listener);
|
||||||
setFavourited(status.getFavourited());
|
setAvatar(realStatus.account.avatar);
|
||||||
String rebloggedByDisplayName = status.getRebloggedByDisplayName();
|
setReblogged(realStatus.reblogged);
|
||||||
if (rebloggedByDisplayName == null) {
|
setFavourited(realStatus.favourited);
|
||||||
|
String rebloggedByDisplayName = status.account.displayName;
|
||||||
|
if (status.reblog == null) {
|
||||||
hideRebloggedByDisplayName();
|
hideRebloggedByDisplayName();
|
||||||
} else {
|
} else {
|
||||||
setRebloggedByDisplayName(rebloggedByDisplayName);
|
setRebloggedByDisplayName(rebloggedByDisplayName);
|
||||||
}
|
}
|
||||||
Status.MediaAttachment[] attachments = status.getAttachments();
|
Status.MediaAttachment[] attachments = realStatus.attachments;
|
||||||
boolean sensitive = status.getSensitive();
|
boolean sensitive = realStatus.sensitive;
|
||||||
setMediaPreviews(attachments, sensitive, listener);
|
setMediaPreviews(attachments, sensitive, listener);
|
||||||
/* A status without attachments is sometimes still marked sensitive, so it's necessary to
|
/* 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. */
|
* check both whether there are any attachments and if it's marked sensitive. */
|
||||||
if (!sensitive || attachments.length == 0) {
|
if (!sensitive || attachments.length == 0) {
|
||||||
hideSensitiveMediaWarning();
|
hideSensitiveMediaWarning();
|
||||||
}
|
}
|
||||||
setupButtons(listener, status.getAccountId());
|
setupButtons(listener, realStatus.account.id);
|
||||||
setRebloggingEnabled(status.getVisibility() != Status.Visibility.PRIVATE);
|
setRebloggingEnabled(realStatus.visibility != Status.Visibility.PRIVATE);
|
||||||
if (status.getSpoilerText().isEmpty()) {
|
if (realStatus.spoilerText.isEmpty()) {
|
||||||
hideSpoilerText();
|
hideSpoilerText();
|
||||||
} else {
|
} else {
|
||||||
setSpoilerText(status.getSpoilerText());
|
setSpoilerText(realStatus.spoilerText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,6 +20,8 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import com.android.volley.AuthFailureError;
|
||||||
import com.android.volley.Response;
|
import com.android.volley.Response;
|
||||||
import com.android.volley.VolleyError;
|
import com.android.volley.VolleyError;
|
||||||
import com.android.volley.toolbox.JsonArrayRequest;
|
import com.android.volley.toolbox.JsonArrayRequest;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
@ -39,6 +40,9 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
|
||||||
public class TimelineFragment extends SFragment implements
|
public class TimelineFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener {
|
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener {
|
||||||
private static final String TAG = "Timeline"; // logging tag and Volley request tag
|
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();
|
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
|
||||||
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
sendFetchTimelineRequest(status.getId());
|
sendFetchTimelineRequest(status.id);
|
||||||
} else {
|
} else {
|
||||||
sendFetchTimelineRequest();
|
sendFetchTimelineRequest();
|
||||||
}
|
}
|
||||||
|
@ -168,67 +172,43 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendFetchTimelineRequest(final String fromId) {
|
private void sendFetchTimelineRequest(final String fromId) {
|
||||||
String endpoint;
|
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
|
||||||
|
|
||||||
|
Callback<List<Status>> cb = new Callback<List<Status>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<List<Status>> call, retrofit2.Response<List<Status>> response) {
|
||||||
|
onFetchTimelineSuccess(response.body(), fromId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<List<Status>> call, Throwable t) {
|
||||||
|
onFetchTimelineFailure((Exception) t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
default:
|
default:
|
||||||
case HOME: {
|
case HOME: {
|
||||||
endpoint = getString(R.string.endpoint_timelines_home);
|
api.homeTimeline(fromId, null, null).enqueue(cb);
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MENTIONS: {
|
|
||||||
endpoint = getString(R.string.endpoint_timelines_mentions);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PUBLIC: {
|
case PUBLIC: {
|
||||||
endpoint = getString(R.string.endpoint_timelines_public);
|
api.publicTimeline(null, fromId, null, null).enqueue(cb);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TAG: {
|
case TAG: {
|
||||||
endpoint = String.format(getString(R.string.endpoint_timelines_tag), hashtagOrId);
|
api.hashtagTimeline(hashtagOrId, null, fromId, null, null).enqueue(cb);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case USER: {
|
case USER: {
|
||||||
endpoint = String.format(getString(R.string.endpoint_statuses), hashtagOrId);
|
api.accountStatuses(hashtagOrId, fromId, null, null).enqueue(cb);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case FAVOURITES: {
|
case FAVOURITES: {
|
||||||
endpoint = getString(R.string.endpoint_favourites);
|
api.favourites(fromId, null, null).enqueue(cb);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String url = "https://" + domain + endpoint;
|
|
||||||
if (fromId != null) {
|
|
||||||
url += "?max_id=" + fromId;
|
|
||||||
}
|
|
||||||
JsonArrayRequest request = new JsonArrayRequest(url,
|
|
||||||
new Response.Listener<JSONArray>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(JSONArray response) {
|
|
||||||
List<Status> 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<String, String> getHeaders() throws AuthFailureError {
|
|
||||||
Map<String, String> headers = new HashMap<>();
|
|
||||||
headers.put("Authorization", "Bearer " + accessToken);
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.setTag(TAG);
|
|
||||||
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendFetchTimelineRequest() {
|
private void sendFetchTimelineRequest() {
|
||||||
|
@ -237,7 +217,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
|
|
||||||
private static boolean findStatus(List<Status> statuses, String id) {
|
private static boolean findStatus(List<Status> statuses, String id) {
|
||||||
for (Status status : statuses) {
|
for (Status status : statuses) {
|
||||||
if (status.getId().equals(id)) {
|
if (status.id.equals(id)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,7 +261,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
public void onLoadMore() {
|
public void onLoadMore() {
|
||||||
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
sendFetchTimelineRequest(status.getId());
|
sendFetchTimelineRequest(status.id);
|
||||||
} else {
|
} else {
|
||||||
sendFetchTimelineRequest();
|
sendFetchTimelineRequest();
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,12 +30,17 @@ import android.view.ViewGroup;
|
||||||
import com.android.volley.Request;
|
import com.android.volley.Request;
|
||||||
import com.android.volley.Response;
|
import com.android.volley.Response;
|
||||||
import com.android.volley.VolleyError;
|
import com.android.volley.VolleyError;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.entity.StatusContext;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
|
||||||
public class ViewThreadFragment extends SFragment implements StatusActionListener {
|
public class ViewThreadFragment extends SFragment implements StatusActionListener {
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private ThreadAdapter adapter;
|
private ThreadAdapter adapter;
|
||||||
|
@ -78,54 +83,39 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendStatusRequest(final String id) {
|
private void sendStatusRequest(final String id) {
|
||||||
String endpoint = String.format(getString(R.string.endpoint_get_status), id);
|
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
|
||||||
super.sendRequest(Request.Method.GET, endpoint, null,
|
|
||||||
new Response.Listener<JSONObject>() {
|
api.status(id).enqueue(new Callback<Status>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(JSONObject response) {
|
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
||||||
Status status;
|
int position = adapter.insertStatus(response.body());
|
||||||
try {
|
recyclerView.scrollToPosition(position);
|
||||||
status = Status.parse(response, false);
|
}
|
||||||
} catch (JSONException e) {
|
|
||||||
onThreadRequestFailure(id);
|
@Override
|
||||||
return;
|
public void onFailure(Call<Status> call, Throwable t) {
|
||||||
}
|
onThreadRequestFailure(id);
|
||||||
int position = adapter.insertStatus(status);
|
}
|
||||||
recyclerView.scrollToPosition(position);
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
new Response.ErrorListener() {
|
|
||||||
@Override
|
|
||||||
public void onErrorResponse(VolleyError error) {
|
|
||||||
onThreadRequestFailure(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendThreadRequest(final String id) {
|
private void sendThreadRequest(final String id) {
|
||||||
String endpoint = String.format(getString(R.string.endpoint_context), id);
|
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
|
||||||
super.sendRequest(Request.Method.GET, endpoint, null,
|
|
||||||
new Response.Listener<JSONObject>() {
|
api.statusContext(id).enqueue(new Callback<StatusContext>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(JSONObject response) {
|
public void onResponse(Call<StatusContext> call, retrofit2.Response<StatusContext> response) {
|
||||||
try {
|
StatusContext context = response.body();
|
||||||
List<Status> ancestors =
|
|
||||||
Status.parse(response.getJSONArray("ancestors"));
|
adapter.addAncestors(context.ancestors);
|
||||||
List<Status> descendants =
|
adapter.addDescendants(context.descendants);
|
||||||
Status.parse(response.getJSONArray("descendants"));
|
}
|
||||||
adapter.addAncestors(ancestors);
|
|
||||||
adapter.addDescendants(descendants);
|
@Override
|
||||||
} catch (JSONException e) {
|
public void onFailure(Call<StatusContext> call, Throwable t) {
|
||||||
onThreadRequestFailure(id);
|
onThreadRequestFailure(id);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
},
|
|
||||||
new Response.ErrorListener() {
|
|
||||||
@Override
|
|
||||||
public void onErrorResponse(VolleyError error) {
|
|
||||||
onThreadRequestFailure(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onThreadRequestFailure(final String id) {
|
private void onThreadRequestFailure(final String id) {
|
||||||
|
@ -162,7 +152,7 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
|
||||||
|
|
||||||
public void onViewThread(int position) {
|
public void onViewThread(int position) {
|
||||||
Status status = adapter.getItem(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.
|
// If already viewing this thread, don't reopen it.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
122
app/src/main/java/com/keylesspalace/tusky/entity/Status.java
Normal file
122
app/src/main/java/com/keylesspalace/tusky/entity/Status.java
Normal file
|
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
package com.keylesspalace.tusky.entity;
|
package com.keylesspalace.tusky.entity;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.Status;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class StatusContext {
|
public class StatusContext {
|
||||||
|
|
Loading…
Reference in a new issue