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) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_account);
|
||||
createMastodonAPI();
|
||||
|
||||
Intent intent = getIntent();
|
||||
accountId = intent.getStringExtra("id");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<List<Account>> favourites(
|
||||
Call<List<Status>> favourites(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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);
|
||||
}
|
||||
}) {
|
||||
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
|
||||
|
||||
api.notifications(fromId, null, null).enqueue(new Callback<List<Notification>>() {
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + accessToken);
|
||||
return headers;
|
||||
public void onResponse(Call<List<Notification>> call, retrofit2.Response<List<Notification>> response) {
|
||||
onFetchNotificationsSuccess(response.body(), fromId);
|
||||
}
|
||||
};
|
||||
request.setTag(TAG);
|
||||
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Notification>> call, Throwable t) {
|
||||
onFetchNotificationsFailure((Exception) t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendFetchNotificationsRequest() {
|
||||
|
@ -174,7 +161,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
private static boolean findNotification(List<Notification> 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) {
|
||||
|
|
|
@ -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<JSONArray>() {
|
||||
final String lastUpdateId) {
|
||||
OkHttpClient okHttpClient = new OkHttpClient.Builder()
|
||||
.addInterceptor(new Interceptor() {
|
||||
@Override
|
||||
public void onResponse(JSONArray response) {
|
||||
List<Notification> 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<List<Notification>>() {
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + accessToken);
|
||||
return headers;
|
||||
public void onResponse(Call<List<Notification>> call, retrofit2.Response<List<Notification>> response) {
|
||||
onCheckNotificationsSuccess(response.body(), lastUpdateId);
|
||||
}
|
||||
};
|
||||
request.setTag(TAG);
|
||||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Notification>> call, Throwable t) {
|
||||
onCheckNotificationsFailure((Exception) t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onCheckNotificationsSuccess(List<Notification> notifications, Date lastUpdate) {
|
||||
Date newest = null;
|
||||
private void onCheckNotificationsSuccess(List<com.keylesspalace.tusky.entity.Notification> notifications, String lastUpdateId) {
|
||||
List<MentionResult> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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);
|
||||
}
|
||||
}) {
|
||||
mastodonAPI.accountStatuses(accountId, null, null, null).enqueue(new Callback<List<Status>>() {
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + accessToken);
|
||||
return headers;
|
||||
public void onResponse(Call<List<Status>> call, retrofit2.Response<List<Status>> response) {
|
||||
List<Status> statusList = response.body();
|
||||
List<ReportAdapter.ReportStatus> 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<List<Status>> call, Throwable t) {
|
||||
onFetchStatusesFailure((Exception) t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onFetchStatusesFailure(Exception exception) {
|
||||
|
|
|
@ -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<String> 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<JSONObject>() {
|
||||
@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<JSONObject>() {
|
||||
@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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
interface StatusActionListener {
|
||||
void onReply(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.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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<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) {
|
||||
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<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() {
|
||||
|
@ -237,7 +217,7 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
private static boolean findStatus(List<Status> 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();
|
||||
}
|
||||
|
|
|
@ -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<JSONObject>() {
|
||||
@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<Status>() {
|
||||
@Override
|
||||
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
||||
int position = adapter.insertStatus(response.body());
|
||||
recyclerView.scrollToPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Status> 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<JSONObject>() {
|
||||
@Override
|
||||
public void onResponse(JSONObject response) {
|
||||
try {
|
||||
List<Status> ancestors =
|
||||
Status.parse(response.getJSONArray("ancestors"));
|
||||
List<Status> 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<StatusContext>() {
|
||||
@Override
|
||||
public void onResponse(Call<StatusContext> call, retrofit2.Response<StatusContext> response) {
|
||||
StatusContext context = response.body();
|
||||
|
||||
adapter.addAncestors(context.ancestors);
|
||||
adapter.addDescendants(context.descendants);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<StatusContext> 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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
import com.keylesspalace.tusky.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class StatusContext {
|
||||
|
|
Loading…
Reference in a new issue