From 370b1e52aaf15d799af17e2d554a819072a7b954 Mon Sep 17 00:00:00 2001 From: Vavassor Date: Sat, 7 Jan 2017 17:24:02 -0500 Subject: [PATCH] added a basic compose screen, and the 3 main timelines in a tabbed layout --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 3 + .../keylesspalace/tusky/ComposeActivity.java | 137 ++++++++ .../keylesspalace/tusky/LoginActivity.java | 5 +- .../com/keylesspalace/tusky/MainActivity.java | 229 ++----------- .../com/keylesspalace/tusky/Notification.java | 43 +++ .../tusky/NotificationsAdapter.java | 99 ++++++ .../tusky/NotificationsFragment.java | 157 +++++++++ .../java/com/keylesspalace/tusky/Status.java | 134 +++++++- .../tusky/StatusActionListener.java | 9 + .../keylesspalace/tusky/TimelineAdapter.java | 87 ++++- .../keylesspalace/tusky/TimelineFragment.java | 315 ++++++++++++++++++ .../tusky/TimelinePagerAdapter.java | 45 +++ app/src/main/res/drawable/ic_compose.xml | 11 + app/src/main/res/drawable/ic_extra.xml | 15 + .../main/res/drawable/ic_favourite_off.xml | 7 + app/src/main/res/drawable/ic_favourite_on.xml | 7 + .../main/res/drawable/ic_reblog_disabled.xml | 11 + app/src/main/res/drawable/ic_reblog_off.xml | 11 + app/src/main/res/drawable/ic_reblog_on.xml | 11 + app/src/main/res/drawable/ic_reply.xml | 7 + app/src/main/res/layout/activity_compose.xml | 69 ++++ app/src/main/res/layout/activity_main.xml | 30 +- app/src/main/res/layout/fragment_timeline.xml | 13 + app/src/main/res/layout/item_notification.xml | 11 + app/src/main/res/layout/item_status.xml | 59 ++++ app/src/main/res/menu/main_toolbar.xml | 8 +- app/src/main/res/menu/status_more.xml | 8 + .../main/res/menu/status_more_for_user.xml | 6 + app/src/main/res/values/strings.xml | 52 ++- 30 files changed, 1367 insertions(+), 234 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/Notification.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/TimelinePagerAdapter.java create mode 100644 app/src/main/res/drawable/ic_compose.xml create mode 100644 app/src/main/res/drawable/ic_extra.xml create mode 100644 app/src/main/res/drawable/ic_favourite_off.xml create mode 100644 app/src/main/res/drawable/ic_favourite_on.xml create mode 100644 app/src/main/res/drawable/ic_reblog_disabled.xml create mode 100644 app/src/main/res/drawable/ic_reblog_off.xml create mode 100644 app/src/main/res/drawable/ic_reblog_on.xml create mode 100644 app/src/main/res/drawable/ic_reply.xml create mode 100644 app/src/main/res/layout/activity_compose.xml create mode 100644 app/src/main/res/layout/fragment_timeline.xml create mode 100644 app/src/main/res/layout/item_notification.xml create mode 100644 app/src/main/res/menu/status_more.xml create mode 100644 app/src/main/res/menu/status_more_for_user.xml diff --git a/app/build.gradle b/app/build.gradle index 347693658..3d0cdce28 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d8bfc20aa..272b993ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,6 +27,9 @@ + \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java new file mode 100644 index 000000000..8717305cc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -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() { + @Override + public void onResponse(JSONObject response) { + onSendSuccess(); + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + onSendFailure(error); + } + }) { + @Override + public Map getHeaders() throws AuthFailureError { + Map 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)); + } + } + }); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java index 24a15975e..8a0279089 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java @@ -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; diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index c43112d16..16e9e8fd6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -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

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 parseStatuses(JSONArray array) throws JSONException { - List 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() { - @Override - public void onResponse(JSONArray response) { - List 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 getHeaders() throws AuthFailureError { - Map headers = new HashMap<>(); - headers.put("Authorization", "Bearer " + accessToken); - return headers; - } - - @Override - protected Map getParams() throws AuthFailureError { - Map parameters = new HashMap<>(); - parameters.put("max_id", fromId); - return parameters; - } - }; - VolleySingleton.getInstance(this).addToRequestQueue(request); - } - - private void sendFetchTimelineRequest() { - sendFetchTimelineRequest(null); - } - - public void onFetchTimelineSuccess(List 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); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/Notification.java b/app/src/main/java/com/keylesspalace/tusky/Notification.java new file mode 100644 index 000000000..d8ab1dcdd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/Notification.java @@ -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; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java new file mode 100644 index 000000000..066fea26d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java @@ -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 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 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 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); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java new file mode 100644 index 000000000..c0e9af636 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java @@ -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() { + @Override + public void onResponse(JSONArray response) { + List 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 getHeaders() throws AuthFailureError { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + accessToken); + return headers; + } + }; + VolleySingleton.getInstance(getContext()).addToRequestQueue(request); + } + + private void sendFetchNotificationsRequest() { + sendFetchNotificationsRequest(null); + } + + private void onFetchNotificationsSuccess(List 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(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/Status.java b/app/src/main/java/com/keylesspalace/tusky/Status.java index 4eef6ea30..d69855a3f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/Status.java @@ -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

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 parse(JSONArray array) throws JSONException { + List statuses = new ArrayList<>(); + for (int i = 0; i < array.length(); i++) { + JSONObject object = array.getJSONObject(i); + statuses.add(parse(object, false)); + } + return statuses; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java new file mode 100644 index 000000000..fb261bb13 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java @@ -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); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java index 5d4506ee5..41a831699 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java @@ -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 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); + } + }); + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java new file mode 100644 index 000000000..f687e99a0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java @@ -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() { + @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() { + @Override + public void onResponse(JSONArray response) { + List statuses = null; + try { + statuses = Status.parse(response); + } catch (JSONException e) { + onFetchTimelineFailure(e); + } + if (statuses != null) { + onFetchTimelineSuccess(statuses, fromId != null); + } + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + onFetchTimelineFailure(error); + } + }) { + @Override + public Map getHeaders() throws AuthFailureError { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + accessToken); + return headers; + } + }; + VolleySingleton.getInstance(getContext()).addToRequestQueue(request); + } + + private void sendFetchTimelineRequest() { + sendFetchTimelineRequest(null); + } + + public void onFetchTimelineSuccess(List 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 responseListener) { + if (responseListener == null) { + // Use a dummy listener if one wasn't specified so the request can be constructed. + responseListener = new Response.Listener() { + @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 getHeaders() throws AuthFailureError { + Map 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() { + @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() { + @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(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelinePagerAdapter.java b/app/src/main/java/com/keylesspalace/tusky/TimelinePagerAdapter.java new file mode 100644 index 000000000..7bac4e046 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/TimelinePagerAdapter.java @@ -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]; + } +} diff --git a/app/src/main/res/drawable/ic_compose.xml b/app/src/main/res/drawable/ic_compose.xml new file mode 100644 index 000000000..c5669bb7a --- /dev/null +++ b/app/src/main/res/drawable/ic_compose.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_extra.xml b/app/src/main/res/drawable/ic_extra.xml new file mode 100644 index 000000000..a87c95d53 --- /dev/null +++ b/app/src/main/res/drawable/ic_extra.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_favourite_off.xml b/app/src/main/res/drawable/ic_favourite_off.xml new file mode 100644 index 000000000..7a171bea2 --- /dev/null +++ b/app/src/main/res/drawable/ic_favourite_off.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_favourite_on.xml b/app/src/main/res/drawable/ic_favourite_on.xml new file mode 100644 index 000000000..7ebaaee58 --- /dev/null +++ b/app/src/main/res/drawable/ic_favourite_on.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_reblog_disabled.xml b/app/src/main/res/drawable/ic_reblog_disabled.xml new file mode 100644 index 000000000..8d1960db3 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_disabled.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_reblog_off.xml b/app/src/main/res/drawable/ic_reblog_off.xml new file mode 100644 index 000000000..bae42451b --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_off.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_reblog_on.xml b/app/src/main/res/drawable/ic_reblog_on.xml new file mode 100644 index 000000000..516a346be --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_on.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_reply.xml b/app/src/main/res/drawable/ic_reply.xml new file mode 100644 index 000000000..03247650f --- /dev/null +++ b/app/src/main/res/drawable/ic_reply.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml new file mode 100644 index 000000000..a02f76fd4 --- /dev/null +++ b/app/src/main/res/layout/activity_compose.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + +