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
|
@ -10,6 +10,7 @@ android {
|
|||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
@ -28,4 +29,5 @@ dependencies {
|
|||
compile 'com.android.support:recyclerview-v7:25.1.0'
|
||||
compile 'com.android.volley:volley:1.0.0'
|
||||
testCompile 'junit:junit:4.12'
|
||||
compile 'com.android.support:design:25.1.0'
|
||||
}
|
||||
|
|
|
@ -27,6 +27,9 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".MainActivity" />
|
||||
<activity
|
||||
android:name=".ComposeActivity"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
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,227 +3,40 @@ 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;
|
||||
private void compose() {
|
||||
Intent intent = new Intent(this, ComposeActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
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(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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];
|
||||
}
|
||||
}
|
11
app/src/main/res/drawable/ic_compose.xml
Normal file
11
app/src/main/res/drawable/ic_compose.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:height="24dp" android:viewportHeight="708.66144"
|
||||
android:viewportWidth="708.66144" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="m86.4,17.7c-37.3,0 -67.3,30 -67.3,67.3l0,538.6c0,37.3 30,67.3 67.3,67.3l537.2,0c37.3,0 67.3,-30 67.3,-67.3l0,-462.4c-17.6,17.9 -35.4,35.6 -53.2,53.3l0,352.4c0,39.3 -31.6,70.9 -70.9,70.9l-425.2,0c-39.3,0 -70.9,-31.6 -70.9,-70.9l0,-425.2c0,-39.3 31.6,-70.9 70.9,-70.9l358.9,0c18,-17.7 36,-35.3 53.8,-53.2l-468,0z"
|
||||
android:strokeAlpha="1" android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="33.62945938"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||
android:pathData="m672.8,8.2 l25.1,25.1c13.9,13.9 13.9,36.2 0,50.1L361.6,420.4C347.1,434.2 199.5,537.2 185.6,523.3l-0.8,-0.8C170.9,508.6 272.1,359.2 286.6,344.7L622.7,8.2c13.9,-13.9 36.2,-13.9 50.1,0z"
|
||||
android:strokeAlpha="1" android:strokeColor="#cccccc"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/>
|
||||
</vector>
|
15
app/src/main/res/drawable/ic_extra.xml
Normal file
15
app/src/main/res/drawable/ic_extra.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<vector android:height="24dp" android:viewportHeight="708.66144"
|
||||
android:viewportWidth="708.66144" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#4d4d4d"
|
||||
android:pathData="M177.2,354.3m-70.9,0a70.9,70.9 0,1 1,141.7 0a70.9,70.9 0,1 1,-141.7 0"
|
||||
android:strokeAlpha="1" android:strokeColor="#6d6d6d"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="14.17322826"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#4d4d4d"
|
||||
android:pathData="M354.3,354.3m-70.9,0a70.9,70.9 0,1 1,141.7 0a70.9,70.9 0,1 1,-141.7 0"
|
||||
android:strokeAlpha="1" android:strokeColor="#6d6d6d"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="14.17322826"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#4d4d4d"
|
||||
android:pathData="M531.5,354.3m-70.9,0a70.9,70.9 0,1 1,141.7 0a70.9,70.9 0,1 1,-141.7 0"
|
||||
android:strokeAlpha="1" android:strokeColor="#6d6d6d"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="14.17322826"/>
|
||||
</vector>
|
7
app/src/main/res/drawable/ic_favourite_off.xml
Normal file
7
app/src/main/res/drawable/ic_favourite_off.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector android:height="24dp" android:viewportHeight="708.66144"
|
||||
android:viewportWidth="708.66144" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#4d4d4d"
|
||||
android:pathData="M354.3,33.9 L458.4,244.9 691.2,278.7 522.8,442.9 562.5,674.7 354.3,565.3 146.1,674.7 185.9,442.9 17.5,278.7 250.2,244.9Z"
|
||||
android:strokeAlpha="1" android:strokeColor="#6d6d6d"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="33.33507919"/>
|
||||
</vector>
|
7
app/src/main/res/drawable/ic_favourite_on.xml
Normal file
7
app/src/main/res/drawable/ic_favourite_on.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector android:height="24dp" android:viewportHeight="708.66144"
|
||||
android:viewportWidth="708.66144" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#feef00"
|
||||
android:pathData="M354.3,33.9 L458.4,244.9 691.2,278.7 522.8,442.9 562.5,674.7 354.3,565.3 146.1,674.7 185.9,442.9 17.5,278.7 250.2,244.9Z"
|
||||
android:strokeAlpha="1" android:strokeColor="#feef00"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="33.33507919"/>
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_reblog_disabled.xml
Normal file
11
app/src/main/res/drawable/ic_reblog_disabled.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:height="24dp" android:viewportHeight="708.66144"
|
||||
android:viewportWidth="708.66144" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#cccccc"
|
||||
android:pathData="M141.1,141.7L-0.6,283.5L105.7,283.5L105.7,527.5L176.5,456.7L176.5,283.5L282.8,283.5L141.1,141.7zM213.5,141.7L284.4,212.6L420.6,212.6L491.5,141.7L213.5,141.7zM603.3,182.8L532.4,253.7L532.4,460.6L426.1,460.6L567.9,602.4L709.6,460.6L603.3,460.6L603.3,182.8zM254.6,531.5L183.8,602.4L495.4,602.4L424.6,531.5L254.6,531.5z"
|
||||
android:strokeAlpha="1" android:strokeColor="#cccccc"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="15"/>
|
||||
<path android:fillAlpha="1" android:fillColor="#cccccc"
|
||||
android:pathData="M680,28.6L680,28.6A35.4,35.4 76.1,0 1,680 78.7L78.7,680A35.4,35.4 90.3,0 1,28.6 680L28.6,680A35.4,35.4 90.3,0 1,28.6 629.9L629.9,28.6A35.4,35.4 76.1,0 1,680 28.6z"
|
||||
android:strokeAlpha="1" android:strokeColor="#bebebe"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/>
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_reblog_off.xml
Normal file
11
app/src/main/res/drawable/ic_reblog_off.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:height="24dp" android:viewportHeight="708.66144"
|
||||
android:viewportWidth="708.66144" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#4d4d4d"
|
||||
android:pathData="m567.9,602.4 l-141.7,-141.7 106.3,0 0,-248 -248,0 -70.9,-70.9 389.8,0 0,318.9 106.3,0z"
|
||||
android:strokeAlpha="1" android:strokeColor="#6d6d6d"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="14.17322835"/>
|
||||
<path android:fillColor="#4d4d4d"
|
||||
android:pathData="m141.1,141.7 l141.7,141.7 -106.3,0 0,248 248,0 70.9,70.9 -389.8,0 0,-318.9 -106.3,0z"
|
||||
android:strokeAlpha="1" android:strokeColor="#6d6d6d"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="14.17322826"/>
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_reblog_on.xml
Normal file
11
app/src/main/res/drawable/ic_reblog_on.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:height="24dp" android:viewportHeight="708.66144"
|
||||
android:viewportWidth="708.66144" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#008080"
|
||||
android:pathData="m567.9,602.4 l-141.7,-141.7 106.3,0 0,-248 -248,0 -70.9,-70.9 389.8,0 0,318.9 106.3,0z"
|
||||
android:strokeAlpha="1" android:strokeColor="#3c9b9e"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="14.173"/>
|
||||
<path android:fillColor="#008080"
|
||||
android:pathData="m141.1,141.7 l141.7,141.7 -106.3,0 0,248 248,0 70.9,70.9 -389.8,0 0,-318.9 -106.3,0z"
|
||||
android:strokeAlpha="1" android:strokeColor="#3c9b9e"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="14.17322826"/>
|
||||
</vector>
|
7
app/src/main/res/drawable/ic_reply.xml
Normal file
7
app/src/main/res/drawable/ic_reply.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector android:height="24dp" android:viewportHeight="708.66144"
|
||||
android:viewportWidth="708.66144" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="1" android:fillColor="#4d4d4d"
|
||||
android:pathData="m40.4,348.7c200,-200 200,-200 200,-200l0,120 440,0 0,320 -160,0 0,-160 -280,0 0,120z"
|
||||
android:strokeAlpha="1" android:strokeColor="#6d6d6d"
|
||||
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="21.25984252"/>
|
||||
</vector>
|
69
app/src/main/res/layout/activity_compose.xml
Normal file
69
app/src/main/res/layout/activity_compose.xml
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<EditText
|
||||
android:layout_width="match_parent"
|
||||
android:inputType="textMultiLine"
|
||||
android:ems="10"
|
||||
android:gravity="top|left"
|
||||
android:id="@+id/field_status"
|
||||
android:contentDescription="What's Happening?"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RadioGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:id="@+id/radio_visibility"
|
||||
android:checkedButton="@+id/radio_public">
|
||||
|
||||
<RadioButton
|
||||
android:text="@string/visibility_public"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/radio_public"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<RadioButton
|
||||
android:text="@string/visibility_unlisted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/radio_unlisted"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<RadioButton
|
||||
android:text="@string/visibility_private"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/radio_private"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/characters_left"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="500" />
|
||||
|
||||
<Button
|
||||
android:text="TOOT"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/button_send" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -19,18 +19,34 @@
|
|||
android:background="?attr/colorPrimary"
|
||||
android:elevation="4dp"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
|
||||
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh_layout"
|
||||
<android.support.v4.view.ViewPager
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
<android.support.design.widget.TabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
</android.support.v4.widget.SwipeRefreshLayout>
|
||||
<android.support.design.widget.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_home" />
|
||||
|
||||
<android.support.design.widget.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_notifications" />
|
||||
|
||||
<android.support.design.widget.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_public" />
|
||||
</android.support.design.widget.TabLayout>
|
||||
|
||||
</android.support.v4.view.ViewPager>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
13
app/src/main/res/layout/fragment_timeline.xml
Normal file
13
app/src/main/res/layout/fragment_timeline.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v4.widget.SwipeRefreshLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/swipe_refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</android.support.v4.widget.SwipeRefreshLayout>
|
11
app/src/main/res/layout/item_notification.xml
Normal file
11
app/src/main/res/layout/item_notification.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
|
@ -76,4 +76,63 @@
|
|||
android:layout_toEndOf="@+id/status_avatar"
|
||||
android:layout_below="@+id/status_name_bar" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/status_content"
|
||||
android:layout_toRightOf="@+id/status_avatar"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingTop="8dp">
|
||||
|
||||
<ImageButton
|
||||
app:srcCompat="@drawable/ic_reply"
|
||||
android:id="@+id/status_reply"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageButton
|
||||
app:srcCompat="@drawable/ic_reblog_off"
|
||||
android:id="@+id/status_reblog"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
app:srcCompat="@drawable/ic_favourite_off"
|
||||
android:id="@+id/status_favourite"
|
||||
style="?android:attr/borderlessButtonStyle" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageButton
|
||||
app:srcCompat="@drawable/ic_extra"
|
||||
android:id="@+id/status_more"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,7 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/tools">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_compose"
|
||||
android:title="@string/action_compose"
|
||||
android:icon="@drawable/ic_compose"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_logout"
|
||||
|
|
8
app/src/main/res/menu/status_more.xml
Normal file
8
app/src/main/res/menu/status_more.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:id="@+id/status_follow"
|
||||
android:title="@string/action_follow" />
|
||||
<item android:title="@string/action_block"
|
||||
android:id="@+id/status_block" />
|
||||
</menu>
|
6
app/src/main/res/menu/status_more_for_user.xml
Normal file
6
app/src/main/res/menu/status_more_for_user.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:title="@string/action_delete"
|
||||
android:id="@+id/status_delete" />
|
||||
</menu>
|
|
@ -5,15 +5,63 @@
|
|||
<string name="oauth_redirect_host">oauth2redirect</string>
|
||||
<string name="preferences_file_key">com.keylesspalace.tusky.PREFERENCES</string>
|
||||
|
||||
<string name="endpoint_status">/api/v1/statuses</string>
|
||||
<string name="endpoint_media">/api/v1/media</string>
|
||||
<string name="endpoint_timelines_home">/api/v1/timelines/home</string>
|
||||
<string name="endpoint_timelines_mentions">/api/v1/timelines/mentions</string>
|
||||
<string name="endpoint_timelines_public">/api/v1/timelines/public</string>
|
||||
<string name="endpoint_timelines_tag">/api/v1/timelines/tag/%s</string>
|
||||
<string name="endpoint_notifications">/api/v1/notifications</string>
|
||||
<string name="endpoint_follows">/api/v1/follows</string>
|
||||
<string name="endpoint_get_status">/api/v1/statuses/%s</string>
|
||||
<string name="endpoint_accounts">/api/v1/accounts/%s</string>
|
||||
<string name="endpoint_verify_credentials">/api/v1/accounts/verify_credentials</string>
|
||||
<string name="endpoint_statuses">/api/v1/accounts/%s/statuses</string>
|
||||
<string name="endpoint_following">/api/v1/accounts/%s/following</string>
|
||||
<string name="endpoint_followers">/api/v1/accounts/%s/followers</string>
|
||||
<string name="endpoint_relationships">/api/v1/accounts/relationships</string>
|
||||
<string name="endpoint_blocks">/api/v1/blocks</string>
|
||||
<string name="endpoint_favourites">/api/v1/favourites</string>
|
||||
<string name="endpoint_delete">/api/v1/statuses/%s</string>
|
||||
<string name="endpoint_reblog">/api/v1/statuses/%s/reblog</string>
|
||||
<string name="endpoint_unreblog">/api/v1/statuses/%s/unreblog</string>
|
||||
<string name="endpoint_favourite">/api/v1/statuses/%s/favourite</string>
|
||||
<string name="endpoint_unfavourite">/api/v1/statuses/%s/unfavourite</string>
|
||||
<string name="endpoint_context">/api/v1/statuses/%s/context</string>
|
||||
<string name="endpoint_reblogged_by">/api/v1/statuses/%s/reblogged_by</string>
|
||||
<string name="endpoint_favourited_by">/api/v1/statuses/%s/favourited_by</string>
|
||||
<string name="endpoint_follow">/api/v1/accounts/%s/follow</string>
|
||||
<string name="endpoint_unfollow">/api/v1/accounts/%s/unfollow</string>
|
||||
<string name="endpoint_block">/api/v1/accounts/%s/block</string>
|
||||
<string name="endpoint_unblock">/api/v1/accounts/%s/unblock</string>
|
||||
<string name="endpoint_apps">/api/v1/apps</string>
|
||||
<string name="endpoint_authorize">/oauth/authorize</string>
|
||||
<string name="endpoint_token">/oauth/token</string>
|
||||
<string name="endpoint_apps">/api/v1/apps</string>
|
||||
<string name="endpoint_timelines_home">/api/v1/timelines/home</string>
|
||||
|
||||
<string name="error_fetching_timeline">Tusky failed to fetch the timeline.</string>
|
||||
<string name="error_fetching_notifications">Notifications could not be fetched.</string>
|
||||
<string name="error_compose_character_limit">The toot is too long!</string>
|
||||
<string name="error_sending_status">The toot failed to be sent.</string>
|
||||
|
||||
<string name="title_home">Home</string>
|
||||
<string name="title_notifications">Notifications</string>
|
||||
<string name="title_public">Public</string>
|
||||
|
||||
<string name="status_username_format">\@%s</string>
|
||||
<string name="status_boosted_format">%s boosted</string>
|
||||
|
||||
<string name="notification_reblog_format">%s boosted your status</string>
|
||||
<string name="notification_favourite_format">%s favourited your status</string>
|
||||
<string name="notification_follow_format">%s followed you</string>
|
||||
|
||||
<string name="action_compose">Compose</string>
|
||||
<string name="action_logout">Log Out</string>
|
||||
<string name="action_follow">Follow</string>
|
||||
<string name="action_block">Block</string>
|
||||
<string name="action_delete">Delete</string>
|
||||
|
||||
<string name="visibility_public">Public</string>
|
||||
<string name="visibility_private">Private</string>
|
||||
<string name="visibility_unlisted">Unlisted</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue