added a basic compose screen, and the 3 main timelines in a tabbed layout
This commit is contained in:
parent
b78ccb1b49
commit
370b1e52aa
30 changed files with 1367 additions and 234 deletions
137
app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
Normal file
137
app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
Normal file
|
@ -0,0 +1,137 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.JsonObjectRequest;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ComposeActivity extends AppCompatActivity {
|
||||
private static int STATUS_CHARACTER_LIMIT = 500;
|
||||
|
||||
private String domain;
|
||||
private String accessToken;
|
||||
private EditText textEditor;
|
||||
|
||||
private void onSendSuccess() {
|
||||
Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
private void onSendFailure(Exception exception) {
|
||||
textEditor.setError(getString(R.string.error_sending_status));
|
||||
}
|
||||
|
||||
private void sendStatus(String content, String visibility) {
|
||||
String endpoint = getString(R.string.endpoint_status);
|
||||
String url = "https://" + domain + endpoint;
|
||||
JSONObject parameters = new JSONObject();
|
||||
try {
|
||||
parameters.put("status", content);
|
||||
parameters.put("visibility", visibility);
|
||||
} catch (JSONException e) {
|
||||
onSendFailure(e);
|
||||
return;
|
||||
}
|
||||
JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, parameters,
|
||||
new Response.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void onResponse(JSONObject response) {
|
||||
onSendSuccess();
|
||||
}
|
||||
}, new Response.ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
onSendFailure(error);
|
||||
}
|
||||
}) {
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + accessToken);
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_compose);
|
||||
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
domain = preferences.getString("domain", null);
|
||||
accessToken = preferences.getString("accessToken", null);
|
||||
assert(domain != null);
|
||||
assert(accessToken != null);
|
||||
|
||||
textEditor = (EditText) findViewById(R.id.field_status);
|
||||
final TextView charactersLeft = (TextView) findViewById(R.id.characters_left);
|
||||
TextWatcher textEditorWatcher = new TextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
int left = STATUS_CHARACTER_LIMIT - s.length();
|
||||
charactersLeft.setText(Integer.toString(left));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
};
|
||||
textEditor.addTextChangedListener(textEditorWatcher);
|
||||
|
||||
final RadioGroup radio = (RadioGroup) findViewById(R.id.radio_visibility);
|
||||
final Button sendButton = (Button) findViewById(R.id.button_send);
|
||||
sendButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Editable editable = textEditor.getText();
|
||||
if (editable.length() <= STATUS_CHARACTER_LIMIT) {
|
||||
int id = radio.getCheckedRadioButtonId();
|
||||
String visibility;
|
||||
switch (id) {
|
||||
default:
|
||||
case R.id.radio_public: {
|
||||
visibility = "public";
|
||||
break;
|
||||
}
|
||||
case R.id.radio_unlisted: {
|
||||
visibility = "unlisted";
|
||||
break;
|
||||
}
|
||||
case R.id.radio_private: {
|
||||
visibility = "private";
|
||||
break;
|
||||
}
|
||||
}
|
||||
sendStatus(editable.toString(), visibility);
|
||||
} else {
|
||||
textEditor.setError(getString(R.string.error_compose_character_limit));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -26,6 +26,8 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
|
||||
public class LoginActivity extends AppCompatActivity {
|
||||
private static String OAUTH_SCOPES = "read write follow";
|
||||
|
||||
private SharedPreferences preferences;
|
||||
private String domain;
|
||||
private String clientId;
|
||||
|
@ -71,6 +73,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||
parameters.put("client_id", clientId);
|
||||
parameters.put("redirect_uri", redirectUri);
|
||||
parameters.put("response_type", "code");
|
||||
parameters.put("scope", OAUTH_SCOPES);
|
||||
String queryParameters;
|
||||
try {
|
||||
queryParameters = toQueryString(parameters);
|
||||
|
@ -107,7 +110,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||
try {
|
||||
parameters.put("client_name", getString(R.string.app_name));
|
||||
parameters.put("redirect_uris", getOauthRedirectUri());
|
||||
parameters.put("scopes", "read write follow");
|
||||
parameters.put("scopes", OAUTH_SCOPES);
|
||||
} catch (JSONException e) {
|
||||
//TODO: error text????
|
||||
return;
|
||||
|
|
|
@ -3,228 +3,41 @@ package com.keylesspalace.tusky;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.JsonArrayRequest;
|
||||
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements
|
||||
SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
private String domain = null;
|
||||
private String accessToken = null;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private RecyclerView recyclerView;
|
||||
private TimelineAdapter adapter;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private EndlessOnScrollListener scrollListener;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
domain = preferences.getString("domain", null);
|
||||
accessToken = preferences.getString("accessToken", null);
|
||||
assert(domain != null);
|
||||
assert(accessToken != null);
|
||||
|
||||
// Setup the SwipeRefreshLayout.
|
||||
swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
|
||||
swipeRefreshLayout.setOnRefreshListener(this);
|
||||
// Setup the RecyclerView.
|
||||
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
layoutManager = new LinearLayoutManager(this);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
DividerItemDecoration divider = new DividerItemDecoration(
|
||||
this, layoutManager.getOrientation());
|
||||
Drawable drawable = ContextCompat.getDrawable(this, R.drawable.status_divider);
|
||||
divider.setDrawable(drawable);
|
||||
recyclerView.addItemDecoration(divider);
|
||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
|
||||
String fromId = adapter.getItem(adapter.getItemCount() - 1).getId();
|
||||
sendFetchTimelineRequest(fromId);
|
||||
}
|
||||
TimelinePagerAdapter adapter = new TimelinePagerAdapter(getSupportFragmentManager());
|
||||
String[] pageTitles = {
|
||||
getString(R.string.title_home),
|
||||
getString(R.string.title_notifications),
|
||||
getString(R.string.title_public)
|
||||
};
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
adapter = new TimelineAdapter();
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
sendFetchTimelineRequest();
|
||||
adapter.setPageTitles(pageTitles);
|
||||
ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
}
|
||||
|
||||
private 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 void compose() {
|
||||
Intent intent = new Intent(this, ComposeActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private CharSequence trimTrailingWhitespace(CharSequence s) {
|
||||
int i = s.length();
|
||||
do {
|
||||
i--;
|
||||
} while (i >= 0 && Character.isWhitespace(s.charAt(i)));
|
||||
return s.subSequence(0, i + 1);
|
||||
}
|
||||
|
||||
private Spanned compatFromHtml(String html) {
|
||||
Spanned result;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
|
||||
} else {
|
||||
result = Html.fromHtml(html);
|
||||
}
|
||||
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
||||
* all status contents do, so it should be trimmed. */
|
||||
return (Spanned) trimTrailingWhitespace(result);
|
||||
}
|
||||
|
||||
private Status parseStatus(JSONObject object, boolean isReblog) throws JSONException {
|
||||
String id = object.getString("id");
|
||||
String content = object.getString("content");
|
||||
Date createdAt = parseDate(object.getString("created_at"));
|
||||
|
||||
JSONObject account = object.getJSONObject("account");
|
||||
String displayName = account.getString("display_name");
|
||||
String username = account.getString("acct");
|
||||
String avatar = account.getString("avatar");
|
||||
|
||||
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 = parseStatus(reblogObject, true);
|
||||
}
|
||||
}
|
||||
|
||||
Status status;
|
||||
if (reblog != null) {
|
||||
status = reblog;
|
||||
status.setRebloggedByUsername(username);
|
||||
} else {
|
||||
Spanned contentPlus = compatFromHtml(content);
|
||||
status = new Status(id, displayName, username, contentPlus, avatar, createdAt);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private List<Status> parseStatuses(JSONArray array) throws JSONException {
|
||||
List<Status> statuses = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject object = array.getJSONObject(i);
|
||||
statuses.add(parseStatus(object, false));
|
||||
}
|
||||
return statuses;
|
||||
}
|
||||
|
||||
private void sendFetchTimelineRequest(final String fromId) {
|
||||
String endpoint = getString(R.string.endpoint_timelines_home);
|
||||
String url = "https://" + domain + endpoint;
|
||||
JsonArrayRequest request = new JsonArrayRequest(url,
|
||||
new Response.Listener<JSONArray>() {
|
||||
@Override
|
||||
public void onResponse(JSONArray response) {
|
||||
List<Status> statuses = null;
|
||||
try {
|
||||
statuses = parseStatuses(response);
|
||||
} catch (JSONException e) {
|
||||
onFetchTimelineFailure(e);
|
||||
}
|
||||
if (statuses != null) {
|
||||
onFetchTimelineSuccess(statuses, fromId != null);
|
||||
}
|
||||
}
|
||||
}, 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, String> getParams() throws AuthFailureError {
|
||||
Map<String, String> parameters = new HashMap<>();
|
||||
parameters.put("max_id", fromId);
|
||||
return parameters;
|
||||
}
|
||||
};
|
||||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
private void sendFetchTimelineRequest() {
|
||||
sendFetchTimelineRequest(null);
|
||||
}
|
||||
|
||||
public void onFetchTimelineSuccess(List<Status> statuses, boolean added) {
|
||||
if (added) {
|
||||
adapter.addItems(statuses);
|
||||
} else {
|
||||
adapter.update(statuses);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchTimelineFailure(Exception exception) {
|
||||
Toast.makeText(this, R.string.error_fetching_timeline, Toast.LENGTH_SHORT).show();
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onRefresh() {
|
||||
sendFetchTimelineRequest();
|
||||
}
|
||||
|
||||
|
||||
private void logOut() {
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
|
@ -246,13 +59,15 @@ public class MainActivity extends AppCompatActivity implements
|
|||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_compose: {
|
||||
compose();
|
||||
return true;
|
||||
}
|
||||
case R.id.action_logout: {
|
||||
logOut();
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
|
43
app/src/main/java/com/keylesspalace/tusky/Notification.java
Normal file
43
app/src/main/java/com/keylesspalace/tusky/Notification.java
Normal file
|
@ -0,0 +1,43 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
public class Notification {
|
||||
public enum Type {
|
||||
MENTION,
|
||||
REBLOG,
|
||||
FAVOURITE,
|
||||
FOLLOW,
|
||||
}
|
||||
private Type type;
|
||||
private String id;
|
||||
private String displayName;
|
||||
/** Which of the user's statuses has been mentioned, reblogged, or favourited. */
|
||||
private Status status;
|
||||
|
||||
public Notification(Type type, String id, String displayName) {
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public @Nullable Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
private List<Notification> notifications = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_notification, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
ViewHolder holder = (ViewHolder) viewHolder;
|
||||
Notification notification = notifications.get(position);
|
||||
holder.setMessage(notification.getType(), notification.getDisplayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return notifications.size();
|
||||
}
|
||||
|
||||
public Notification getItem(int position) {
|
||||
return notifications.get(position);
|
||||
}
|
||||
|
||||
public int update(List<Notification> new_notifications) {
|
||||
int scrollToPosition;
|
||||
if (notifications == null || notifications.isEmpty()) {
|
||||
notifications = new_notifications;
|
||||
scrollToPosition = 0;
|
||||
} else {
|
||||
int index = new_notifications.indexOf(notifications.get(0));
|
||||
if (index == -1) {
|
||||
notifications.addAll(0, new_notifications);
|
||||
scrollToPosition = 0;
|
||||
} else {
|
||||
notifications.addAll(0, new_notifications.subList(0, index));
|
||||
scrollToPosition = index;
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
return scrollToPosition;
|
||||
}
|
||||
|
||||
public void addItems(List<Notification> new_notifications) {
|
||||
int end = notifications.size();
|
||||
notifications.addAll(new_notifications);
|
||||
notifyItemRangeInserted(end, new_notifications.size());
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private TextView message;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
message = (TextView) itemView.findViewById(R.id.notification_text);
|
||||
}
|
||||
|
||||
public void setMessage(Notification.Type type, String displayName) {
|
||||
Context context = message.getContext();
|
||||
String wholeMessage = "";
|
||||
switch (type) {
|
||||
case MENTION: {
|
||||
wholeMessage = displayName + " mentioned you";
|
||||
break;
|
||||
}
|
||||
case REBLOG: {
|
||||
String format = context.getString(R.string.notification_reblog_format);
|
||||
wholeMessage = String.format(format, displayName);
|
||||
break;
|
||||
}
|
||||
case FAVOURITE: {
|
||||
String format = context.getString(R.string.notification_favourite_format);
|
||||
wholeMessage = String.format(format, displayName);
|
||||
break;
|
||||
}
|
||||
case FOLLOW: {
|
||||
String format = context.getString(R.string.notification_follow_format);
|
||||
wholeMessage = String.format(format, displayName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
message.setText(wholeMessage);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.JsonArrayRequest;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class NotificationsFragment extends Fragment implements
|
||||
SwipeRefreshLayout.OnRefreshListener {
|
||||
private String domain = null;
|
||||
private String accessToken = null;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private NotificationsAdapter adapter;
|
||||
|
||||
public static NotificationsFragment newInstance() {
|
||||
NotificationsFragment fragment = new NotificationsFragment();
|
||||
Bundle arguments = new Bundle();
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
||||
|
||||
Context context = getContext();
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
domain = preferences.getString("domain", null);
|
||||
accessToken = preferences.getString("accessToken", null);
|
||||
assert(domain != null);
|
||||
assert(accessToken != null);
|
||||
|
||||
// Setup the SwipeRefreshLayout.
|
||||
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
|
||||
swipeRefreshLayout.setOnRefreshListener(this);
|
||||
// Setup the RecyclerView.
|
||||
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
DividerItemDecoration divider = new DividerItemDecoration(
|
||||
context, layoutManager.getOrientation());
|
||||
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.status_divider);
|
||||
divider.setDrawable(drawable);
|
||||
recyclerView.addItemDecoration(divider);
|
||||
EndlessOnScrollListener scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
||||
String fromId = adapter.getItem(adapter.getItemCount() - 1).getId();
|
||||
sendFetchNotificationsRequest(fromId);
|
||||
}
|
||||
};
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
adapter = new NotificationsAdapter();
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
sendFetchNotificationsRequest();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
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) {
|
||||
List<Notification> notifications = new ArrayList<>();
|
||||
try {
|
||||
for (int i = 0; i < response.length(); i++) {
|
||||
JSONObject object = response.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");
|
||||
Notification notification = new Notification(type, id, displayName);
|
||||
notifications.add(notification);
|
||||
}
|
||||
onFetchNotificationsSuccess(notifications, fromId != null);
|
||||
} catch (JSONException e) {
|
||||
onFetchNotificationsFailure(e);
|
||||
}
|
||||
}
|
||||
}, new Response.ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
onFetchNotificationsFailure(error);
|
||||
}
|
||||
}) {
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + accessToken);
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
private void sendFetchNotificationsRequest() {
|
||||
sendFetchNotificationsRequest(null);
|
||||
}
|
||||
|
||||
private void onFetchNotificationsSuccess(List<Notification> notifications, boolean added) {
|
||||
if (added) {
|
||||
adapter.addItems(notifications);
|
||||
} else {
|
||||
adapter.update(notifications);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
private void onFetchNotificationsFailure(Exception exception) {
|
||||
Toast.makeText(getContext(), R.string.error_fetching_notifications, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
sendFetchNotificationsRequest();
|
||||
}
|
||||
}
|
|
@ -1,11 +1,28 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
|
||||
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;
|
||||
|
@ -16,21 +33,35 @@ public class Status {
|
|||
private String rebloggedByUsername;
|
||||
/** 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 Visibility visibility;
|
||||
|
||||
public Status(String id, String displayName, String username, Spanned content, String avatar,
|
||||
Date createdAt) {
|
||||
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.visibility = Visibility.valueOf(visibility.toUpperCase());
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
@ -55,10 +86,30 @@ public class Status {
|
|||
return rebloggedByUsername;
|
||||
}
|
||||
|
||||
public boolean getReblogged() {
|
||||
return reblogged;
|
||||
}
|
||||
|
||||
public boolean getFavourited() {
|
||||
return favourited;
|
||||
}
|
||||
|
||||
public Visibility getVisibility() {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
public void setRebloggedByUsername(String name) {
|
||||
rebloggedByUsername = name;
|
||||
}
|
||||
|
||||
public void setReblogged(boolean reblogged) {
|
||||
this.reblogged = reblogged;
|
||||
}
|
||||
|
||||
public void setFavourited(boolean favourited) {
|
||||
this.favourited = favourited;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
|
@ -74,4 +125,83 @@ public class Status {
|
|||
Status status = (Status) other;
|
||||
return status.id.equals(this.id);
|
||||
}
|
||||
|
||||
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 CharSequence trimTrailingWhitespace(CharSequence s) {
|
||||
int i = s.length();
|
||||
do {
|
||||
i--;
|
||||
} while (i >= 0 && Character.isWhitespace(s.charAt(i)));
|
||||
return s.subSequence(0, i + 1);
|
||||
}
|
||||
|
||||
private static Spanned compatFromHtml(String html) {
|
||||
Spanned result;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
|
||||
} else {
|
||||
result = Html.fromHtml(html);
|
||||
}
|
||||
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
||||
* all status contents do, so it should be trimmed. */
|
||||
return (Spanned) trimTrailingWhitespace(result);
|
||||
}
|
||||
|
||||
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.getBoolean("reblogged");
|
||||
boolean favourited = object.getBoolean("favourited");
|
||||
String visibility = object.getString("visibility");
|
||||
|
||||
JSONObject account = object.getJSONObject("account");
|
||||
String accountId = account.getString("id");
|
||||
String displayName = account.getString("display_name");
|
||||
String username = account.getString("acct");
|
||||
String avatar = account.getString("avatar");
|
||||
|
||||
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.setRebloggedByUsername(username);
|
||||
} else {
|
||||
Spanned contentPlus = compatFromHtml(content);
|
||||
status = new Status(
|
||||
id, accountId, displayName, username, contentPlus, avatar, createdAt,
|
||||
reblogged, favourited, visibility);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
public interface StatusActionListener {
|
||||
void onReblog(final boolean reblog, final int position);
|
||||
void onFavourite(final boolean favourite, final int position);
|
||||
void onMore(View view, final int position);
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
|
@ -21,14 +21,12 @@ import java.util.List;
|
|||
public class TimelineAdapter extends RecyclerView.Adapter {
|
||||
private List<Status> statuses = new ArrayList<>();
|
||||
|
||||
/*
|
||||
TootActionListener listener;
|
||||
StatusActionListener listener;
|
||||
|
||||
public TimelineAdapter(TootActionListener listener) {
|
||||
public TimelineAdapter(StatusActionListener listener) {
|
||||
super();
|
||||
this.listener = listener;
|
||||
}
|
||||
*/
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
|
||||
|
@ -47,13 +45,18 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
holder.setContent(status.getContent());
|
||||
holder.setAvatar(status.getAvatar());
|
||||
holder.setContent(status.getContent());
|
||||
holder.setReblogged(status.getReblogged());
|
||||
holder.setFavourited(status.getFavourited());
|
||||
String rebloggedByUsername = status.getRebloggedByUsername();
|
||||
if (rebloggedByUsername == null) {
|
||||
holder.hideReblogged();
|
||||
holder.hideRebloggedByUsername();
|
||||
} else {
|
||||
holder.setRebloggedByUsername(rebloggedByUsername);
|
||||
}
|
||||
// holder.initButtons(mListener, position);
|
||||
holder.setupButtons(listener, position);
|
||||
if (status.getVisibility() == Status.Visibility.PRIVATE) {
|
||||
holder.disableReblogging();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -86,6 +89,11 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
notifyItemRangeInserted(end, new_statuses.size());
|
||||
}
|
||||
|
||||
public void removeItem(int position) {
|
||||
statuses.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
public Status getItem(int position) {
|
||||
return statuses.get(position);
|
||||
}
|
||||
|
@ -98,6 +106,12 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
private NetworkImageView avatar;
|
||||
private ImageView boostedIcon;
|
||||
private TextView boostedByUsername;
|
||||
private ImageButton replyButton;
|
||||
private ImageButton reblogButton;
|
||||
private ImageButton favouriteButton;
|
||||
private ImageButton moreButton;
|
||||
private boolean favourited;
|
||||
private boolean reblogged;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
@ -108,11 +122,12 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
avatar = (NetworkImageView) itemView.findViewById(R.id.status_avatar);
|
||||
boostedIcon = (ImageView) itemView.findViewById(R.id.status_boosted_icon);
|
||||
boostedByUsername = (TextView) itemView.findViewById(R.id.status_boosted);
|
||||
/*
|
||||
mReplyButton = (ImageButton) itemView.findViewById(R.id.reply);
|
||||
mRetweetButton = (ImageButton) itemView.findViewById(R.id.retweet);
|
||||
mFavoriteButton = (ImageButton) itemView.findViewById(R.id.favorite);
|
||||
*/
|
||||
replyButton = (ImageButton) itemView.findViewById(R.id.status_reply);
|
||||
reblogButton = (ImageButton) itemView.findViewById(R.id.status_reblog);
|
||||
favouriteButton = (ImageButton) itemView.findViewById(R.id.status_favourite);
|
||||
moreButton = (ImageButton) itemView.findViewById(R.id.status_more);
|
||||
reblogged = false;
|
||||
favourited = false;
|
||||
}
|
||||
|
||||
public void setDisplayName(String name) {
|
||||
|
@ -177,7 +192,7 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
long now = new Date().getTime();
|
||||
readout = getRelativeTimeSpanString(then, now);
|
||||
} else {
|
||||
readout = "?m";
|
||||
readout = "?m"; // unknown minutes~
|
||||
}
|
||||
sinceCreated.setText(readout);
|
||||
}
|
||||
|
@ -191,9 +206,53 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
boostedByUsername.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void hideReblogged() {
|
||||
public void hideRebloggedByUsername() {
|
||||
boostedIcon.setVisibility(View.GONE);
|
||||
boostedByUsername.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setReblogged(boolean reblogged) {
|
||||
this.reblogged = reblogged;
|
||||
if (!reblogged) {
|
||||
reblogButton.setImageResource(R.drawable.ic_reblog_off);
|
||||
} else {
|
||||
reblogButton.setImageResource(R.drawable.ic_reblog_on);
|
||||
}
|
||||
}
|
||||
|
||||
public void disableReblogging() {
|
||||
reblogButton.setEnabled(false);
|
||||
reblogButton.setImageResource(R.drawable.ic_reblog_disabled);
|
||||
}
|
||||
|
||||
public void setFavourited(boolean favourited) {
|
||||
this.favourited = favourited;
|
||||
if (!favourited) {
|
||||
favouriteButton.setImageResource(R.drawable.ic_favourite_off);
|
||||
} else {
|
||||
favouriteButton.setImageResource(R.drawable.ic_favourite_on);
|
||||
}
|
||||
}
|
||||
|
||||
public void setupButtons(final StatusActionListener listener, final int position) {
|
||||
reblogButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onReblog(!reblogged, position);
|
||||
}
|
||||
});
|
||||
favouriteButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onFavourite(!favourited, position);
|
||||
}
|
||||
});
|
||||
moreButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onMore(v, position);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
315
app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java
Normal file
315
app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java
Normal file
|
@ -0,0 +1,315 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.PopupMenu;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.JsonArrayRequest;
|
||||
import com.android.volley.toolbox.JsonObjectRequest;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TimelineFragment extends Fragment implements
|
||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener {
|
||||
|
||||
public enum Kind {
|
||||
HOME,
|
||||
MENTIONS,
|
||||
PUBLIC,
|
||||
}
|
||||
|
||||
private String domain = null;
|
||||
private String accessToken = null;
|
||||
private String userAccountId = null;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private TimelineAdapter adapter;
|
||||
private Kind kind;
|
||||
|
||||
public static TimelineFragment newInstance(Kind kind) {
|
||||
TimelineFragment fragment = new TimelineFragment();
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString("kind", kind.name());
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
kind = Kind.valueOf(getArguments().getString("kind"));
|
||||
|
||||
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
||||
|
||||
Context context = getContext();
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
domain = preferences.getString("domain", null);
|
||||
accessToken = preferences.getString("accessToken", null);
|
||||
assert(domain != null);
|
||||
assert(accessToken != null);
|
||||
|
||||
// Setup the SwipeRefreshLayout.
|
||||
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
|
||||
swipeRefreshLayout.setOnRefreshListener(this);
|
||||
// Setup the RecyclerView.
|
||||
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
DividerItemDecoration divider = new DividerItemDecoration(
|
||||
context, layoutManager.getOrientation());
|
||||
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.status_divider);
|
||||
divider.setDrawable(drawable);
|
||||
recyclerView.addItemDecoration(divider);
|
||||
EndlessOnScrollListener scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
|
||||
String fromId = adapter.getItem(adapter.getItemCount() - 1).getId();
|
||||
sendFetchTimelineRequest(fromId);
|
||||
}
|
||||
};
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
adapter = new TimelineAdapter(this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
sendUserInfoRequest();
|
||||
sendFetchTimelineRequest();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private void sendUserInfoRequest() {
|
||||
sendRequest(Request.Method.GET, getString(R.string.endpoint_verify_credentials), null,
|
||||
new Response.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void onResponse(JSONObject response) {
|
||||
try {
|
||||
userAccountId = response.getString("id");
|
||||
} catch (JSONException e) {
|
||||
//TODO: Help
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendFetchTimelineRequest(final String fromId) {
|
||||
String endpoint;
|
||||
switch (kind) {
|
||||
default:
|
||||
case HOME: {
|
||||
endpoint = getString(R.string.endpoint_timelines_home);
|
||||
break;
|
||||
}
|
||||
case MENTIONS: {
|
||||
endpoint = getString(R.string.endpoint_timelines_mentions);
|
||||
break;
|
||||
}
|
||||
case PUBLIC: {
|
||||
endpoint = getString(R.string.endpoint_timelines_public);
|
||||
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 != null);
|
||||
}
|
||||
}
|
||||
}, 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;
|
||||
}
|
||||
};
|
||||
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
private void sendFetchTimelineRequest() {
|
||||
sendFetchTimelineRequest(null);
|
||||
}
|
||||
|
||||
public void onFetchTimelineSuccess(List<Status> statuses, boolean added) {
|
||||
if (added) {
|
||||
adapter.addItems(statuses);
|
||||
} else {
|
||||
adapter.update(statuses);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchTimelineFailure(Exception exception) {
|
||||
Toast.makeText(getContext(), R.string.error_fetching_timeline, Toast.LENGTH_SHORT).show();
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onRefresh() {
|
||||
sendFetchTimelineRequest();
|
||||
}
|
||||
|
||||
private void sendRequest(
|
||||
int method, String endpoint, JSONObject parameters,
|
||||
@Nullable Response.Listener<JSONObject> responseListener) {
|
||||
if (responseListener == null) {
|
||||
// Use a dummy listener if one wasn't specified so the request can be constructed.
|
||||
responseListener = new Response.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void onResponse(JSONObject response) {}
|
||||
};
|
||||
}
|
||||
String url = "https://" + domain + endpoint;
|
||||
JsonObjectRequest request = new JsonObjectRequest(
|
||||
method, url, parameters, responseListener,
|
||||
new Response.ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
System.err.println(error.getMessage());
|
||||
}
|
||||
}) {
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + accessToken);
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
private void postRequest(String endpoint) {
|
||||
sendRequest(Request.Method.POST, endpoint, null, null);
|
||||
}
|
||||
|
||||
public void onReblog(final boolean reblog, final int position) {
|
||||
final Status status = adapter.getItem(position);
|
||||
String id = status.getId();
|
||||
String endpoint;
|
||||
if (reblog) {
|
||||
endpoint = String.format(getString(R.string.endpoint_reblog), id);
|
||||
} else {
|
||||
endpoint = String.format(getString(R.string.endpoint_unreblog), id);
|
||||
}
|
||||
sendRequest(Request.Method.POST, endpoint, null,
|
||||
new Response.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void onResponse(JSONObject response) {
|
||||
status.setReblogged(reblog);
|
||||
adapter.notifyItemChanged(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onFavourite(final boolean favourite, final int position) {
|
||||
final Status status = adapter.getItem(position);
|
||||
String id = status.getId();
|
||||
String endpoint;
|
||||
if (favourite) {
|
||||
endpoint = String.format(getString(R.string.endpoint_favourite), id);
|
||||
} else {
|
||||
endpoint = String.format(getString(R.string.endpoint_unfavourite), id);
|
||||
}
|
||||
sendRequest(Request.Method.POST, endpoint, null, new Response.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void onResponse(JSONObject response) {
|
||||
status.setFavourited(favourite);
|
||||
adapter.notifyItemChanged(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void follow(String id) {
|
||||
String endpoint = String.format(getString(R.string.endpoint_follow), id);
|
||||
postRequest(endpoint);
|
||||
}
|
||||
|
||||
private void block(String id) {
|
||||
String endpoint = String.format(getString(R.string.endpoint_block), id);
|
||||
postRequest(endpoint);
|
||||
}
|
||||
|
||||
private void delete(String id) {
|
||||
String endpoint = String.format(getString(R.string.endpoint_delete), id);
|
||||
sendRequest(Request.Method.DELETE, endpoint, null, null);
|
||||
}
|
||||
|
||||
public void onMore(View view, final int position) {
|
||||
Status status = adapter.getItem(position);
|
||||
final String id = status.getId();
|
||||
final String accountId = status.getAccountId();
|
||||
PopupMenu popup = new PopupMenu(getContext(), view);
|
||||
// Give a different menu depending on whether this is the user's own toot or not.
|
||||
if (userAccountId == null || !userAccountId.equals(accountId)) {
|
||||
popup.inflate(R.menu.status_more);
|
||||
} else {
|
||||
popup.inflate(R.menu.status_more_for_user);
|
||||
}
|
||||
popup.setOnMenuItemClickListener(
|
||||
new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.status_follow: {
|
||||
follow(accountId);
|
||||
return true;
|
||||
}
|
||||
case R.id.status_block: {
|
||||
block(accountId);
|
||||
return true;
|
||||
}
|
||||
case R.id.status_delete: {
|
||||
delete(id);
|
||||
adapter.removeItem(position);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
popup.show();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
|
||||
public class TimelinePagerAdapter extends FragmentPagerAdapter {
|
||||
private String[] pageTitles;
|
||||
|
||||
public TimelinePagerAdapter(FragmentManager manager) {
|
||||
super(manager);
|
||||
}
|
||||
|
||||
public void setPageTitles(String[] titles) {
|
||||
pageTitles = titles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int i) {
|
||||
switch (i) {
|
||||
case 0: {
|
||||
return TimelineFragment.newInstance(TimelineFragment.Kind.HOME);
|
||||
}
|
||||
case 1: {
|
||||
return NotificationsFragment.newInstance();
|
||||
}
|
||||
case 2: {
|
||||
return TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return pageTitles[position];
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue