From b00a3cf44393012cb762d2073d2d55064885d3e2 Mon Sep 17 00:00:00 2001 From: Vavassor Date: Mon, 23 Jan 2017 00:19:30 -0500 Subject: [PATCH] Adds a toot thread viewing mode. Also, many files were missing and didn't push so the previous commits may have been very wrong? --- app/src/main/AndroidManifest.xml | 1 + .../tusky/AdapterItemRemover.java | 5 + .../keylesspalace/tusky/ComposeActivity.java | 2 +- .../com/keylesspalace/tusky/DateUtils.java | 50 +++ .../keylesspalace/tusky/FooterViewHolder.java | 55 ++++ .../com/keylesspalace/tusky/Notification.java | 6 + .../tusky/NotificationsAdapter.java | 184 ++++++++--- .../tusky/NotificationsFragment.java | 12 +- .../com/keylesspalace/tusky/SFragment.java | 247 ++++++++++++++ .../tusky/StatusActionListener.java | 1 + .../keylesspalace/tusky/StatusViewHolder.java | 266 +++++++++++++++ .../keylesspalace/tusky/ThreadAdapter.java | 83 +++++ .../keylesspalace/tusky/TimelineAdapter.java | 306 +----------------- .../keylesspalace/tusky/TimelineFragment.java | 229 ++----------- .../tusky/ViewThreadActivity.java | 68 ++++ .../tusky/ViewThreadFragment.java | 143 ++++++++ app/src/main/res/drawable/boost_icon.png | Bin 221 -> 0 bytes app/src/main/res/drawable/ic_back.xml | 7 + app/src/main/res/drawable/ic_favourited.xml | 7 + app/src/main/res/drawable/ic_followed.xml | 7 + app/src/main/res/drawable/ic_reblogged.xml | 7 + .../main/res/layout/activity_view_thread.xml | 38 +++ .../main/res/layout/fragment_view_thread.xml | 7 + app/src/main/res/layout/item_follow.xml | 31 ++ app/src/main/res/layout/item_notification.xml | 11 - app/src/main/res/layout/item_status.xml | 6 +- .../res/layout/item_status_notification.xml | 39 +++ app/src/main/res/menu/view_thread_toolbar.xml | 11 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 8 +- 31 files changed, 1274 insertions(+), 566 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/DateUtils.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/FooterViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/SFragment.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java delete mode 100644 app/src/main/res/drawable/boost_icon.png create mode 100644 app/src/main/res/drawable/ic_back.xml create mode 100644 app/src/main/res/drawable/ic_favourited.xml create mode 100644 app/src/main/res/drawable/ic_followed.xml create mode 100644 app/src/main/res/drawable/ic_reblogged.xml create mode 100644 app/src/main/res/layout/activity_view_thread.xml create mode 100644 app/src/main/res/layout/fragment_view_thread.xml create mode 100644 app/src/main/res/layout/item_follow.xml delete mode 100644 app/src/main/res/layout/item_notification.xml create mode 100644 app/src/main/res/layout/item_status_notification.xml create mode 100644 app/src/main/res/menu/view_thread_toolbar.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dd187ac7..9dceedbe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,7 @@ android:name=".ComposeActivity" android:windowSoftInputMode="stateVisible|adjustResize" /> + \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java b/app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java new file mode 100644 index 00000000..c52a88bb --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AdapterItemRemover.java @@ -0,0 +1,5 @@ +package com.keylesspalace.tusky; + +public interface AdapterItemRemover { + void removeItem(int position); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index 447f674f..3b6bfb17 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -323,7 +323,7 @@ public class ComposeActivity extends AppCompatActivity { } private void onSendSuccess() { - Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, getString(R.string.confirmation_send), Toast.LENGTH_SHORT).show(); finish(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/DateUtils.java b/app/src/main/java/com/keylesspalace/tusky/DateUtils.java new file mode 100644 index 00000000..202cf353 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/DateUtils.java @@ -0,0 +1,50 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky; + +public class DateUtils { + /* This is a rough duplicate of android.text.format.DateUtils.getRelativeTimeSpanString, + * but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough. */ + public static String getRelativeTimeSpanString(long then, long now) { + final long MINUTE = 60; + final long HOUR = 60 * MINUTE; + final long DAY = 24 * HOUR; + final long YEAR = 365 * DAY; + long span = (now - then) / 1000; + String prefix = ""; + if (span < 0) { + prefix = "in "; + span = -span; + } + String unit; + if (span < MINUTE) { + unit = "s"; + } else if (span < HOUR) { + span /= MINUTE; + unit = "m"; + } else if (span < DAY) { + span /= HOUR; + unit = "h"; + } else if (span < YEAR) { + span /= DAY; + unit = "d"; + } else { + span /= YEAR; + unit = "y"; + } + return prefix + span + unit; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/FooterViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/FooterViewHolder.java new file mode 100644 index 00000000..989e5ffa --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/FooterViewHolder.java @@ -0,0 +1,55 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky; + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ProgressBar; + +public class FooterViewHolder extends RecyclerView.ViewHolder { + private LinearLayout retryBar; + private Button retry; + private ProgressBar progressBar; + + public FooterViewHolder(View itemView) { + super(itemView); + retryBar = (LinearLayout) itemView.findViewById(R.id.footer_retry_bar); + retry = (Button) itemView.findViewById(R.id.footer_retry_button); + progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar); + progressBar.setIndeterminate(true); + } + + public void setupButton(final FooterActionListener listener) { + retry.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onLoadMore(); + } + }); + } + + public void showRetry(boolean show) { + if (!show) { + retryBar.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + } else { + retryBar.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/Notification.java b/app/src/main/java/com/keylesspalace/tusky/Notification.java index 459deeb6..86871cf2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/Notification.java +++ b/app/src/main/java/com/keylesspalace/tusky/Notification.java @@ -55,4 +55,10 @@ public class Notification { public void setStatus(Status status) { this.status = status; } + + public boolean hasStatusType() { + return type == Type.MENTION + || type == Type.FAVOURITE + || type == Type.REBLOG; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java index 7c416608..b776fd0f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsAdapter.java @@ -16,39 +16,128 @@ package com.keylesspalace.tusky; import android.content.Context; +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.ImageView; import android.widget.TextView; import java.util.ArrayList; +import java.util.Date; import java.util.List; -public class NotificationsAdapter extends RecyclerView.Adapter { - private List notifications = new ArrayList<>(); +public class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover { + private static final int VIEW_TYPE_MENTION = 0; + private static final int VIEW_TYPE_FOOTER = 1; + private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2; + private static final int VIEW_TYPE_FOLLOW = 3; + + private List notifications; + private StatusActionListener statusListener; + private FooterActionListener footerListener; + + public NotificationsAdapter(StatusActionListener statusListener, + FooterActionListener footerListener) { + super(); + notifications = new ArrayList<>(); + this.statusListener = statusListener; + this.footerListener = footerListener; + } @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); + switch (viewType) { + default: + case VIEW_TYPE_MENTION: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_status, parent, false); + return new StatusViewHolder(view); + } + case VIEW_TYPE_FOOTER: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new FooterViewHolder(view); + } + case VIEW_TYPE_STATUS_NOTIFICATION: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_status_notification, parent, false); + return new StatusNotificationViewHolder(view); + } + case VIEW_TYPE_FOLLOW: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_follow, parent, false); + return new FollowViewHolder(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()); + if (position < notifications.size()) { + Notification notification = notifications.get(position); + Notification.Type type = notification.getType(); + switch (type) { + case MENTION: { + StatusViewHolder holder = (StatusViewHolder) viewHolder; + Status status = notification.getStatus(); + assert(status != null); + holder.setupWithStatus(status, statusListener, position); + break; + } + case FAVOURITE: + case REBLOG: { + StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder; + holder.setMessage(type, notification.getDisplayName(), + notification.getStatus()); + break; + } + case FOLLOW: { + FollowViewHolder holder = (FollowViewHolder) viewHolder; + holder.setMessage(notification.getDisplayName()); + break; + } + } + } else { + FooterViewHolder holder = (FooterViewHolder) viewHolder; + holder.setupButton(footerListener); + } } @Override public int getItemCount() { - return notifications.size(); + return notifications.size() + 1; } - public Notification getItem(int position) { - return notifications.get(position); + @Override + public int getItemViewType(int position) { + if (position == notifications.size()) { + return VIEW_TYPE_FOOTER; + } else { + Notification notification = notifications.get(position); + switch (notification.getType()) { + default: + case MENTION: { + return VIEW_TYPE_MENTION; + } + case FAVOURITE: + case REBLOG: { + return VIEW_TYPE_STATUS_NOTIFICATION; + } + case FOLLOW: { + return VIEW_TYPE_FOLLOW; + } + } + } + } + + public @Nullable Notification getItem(int position) { + if (position >= 0 && position < notifications.size()) { + return notifications.get(position); + } + return null; } public int update(List new_notifications) { @@ -76,39 +165,62 @@ public class NotificationsAdapter extends RecyclerView.Adapter { notifyItemRangeInserted(end, new_notifications.size()); } - public static class ViewHolder extends RecyclerView.ViewHolder { + public void removeItem(int position) { + notifications.remove(position); + notifyItemChanged(position); + } + + public static class FollowViewHolder extends RecyclerView.ViewHolder { private TextView message; - public ViewHolder(View itemView) { + public FollowViewHolder(View itemView) { super(itemView); message = (TextView) itemView.findViewById(R.id.notification_text); } - public void setMessage(Notification.Type type, String displayName) { + public void setMessage(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; - } - } + String format = context.getString(R.string.notification_follow_format); + String wholeMessage = String.format(format, displayName); message.setText(wholeMessage); } } + + public static class StatusNotificationViewHolder extends RecyclerView.ViewHolder { + private TextView message; + private ImageView icon; + private TextView statusContent; + + public StatusNotificationViewHolder(View itemView) { + super(itemView); + message = (TextView) itemView.findViewById(R.id.notification_text); + icon = (ImageView) itemView.findViewById(R.id.notification_icon); + statusContent = (TextView) itemView.findViewById(R.id.notification_content); + } + + public void setMessage(Notification.Type type, String displayName, Status status) { + Context context = message.getContext(); + String format; + switch (type) { + default: + case FAVOURITE: { + icon.setImageResource(R.drawable.ic_favourited); + format = context.getString(R.string.notification_favourite_format); + break; + } + case REBLOG: { + icon.setImageResource(R.drawable.ic_reblogged); + format = context.getString(R.string.notification_reblog_format); + break; + } + } + String wholeMessage = String.format(format, displayName); + message.setText(wholeMessage); + String timestamp = DateUtils.getRelativeTimeSpanString( + status.getCreatedAt().getTime(), + new Date().getTime()); + statusContent.setText(String.format("%s: ", timestamp)); + statusContent.append(status.getContent()); + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java index 7cc0a9f8..cfe00b4f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/NotificationsFragment.java @@ -172,12 +172,10 @@ public class NotificationsFragment extends SFragment implements } } - @Override public void onRefresh() { sendFetchNotificationsRequest(); } - @Override public void onLoadMore() { Notification notification = adapter.getItem(adapter.getItemCount() - 2); if (notification != null) { @@ -187,32 +185,32 @@ public class NotificationsFragment extends SFragment implements } } - @Override public void onReply(int position) { Notification notification = adapter.getItem(position); super.reply(notification.getStatus()); } - @Override public void onReblog(boolean reblog, int position) { Notification notification = adapter.getItem(position); super.reblog(notification.getStatus(), reblog, adapter, position); } - @Override public void onFavourite(boolean favourite, int position) { Notification notification = adapter.getItem(position); super.favourite(notification.getStatus(), favourite, adapter, position); } - @Override public void onMore(View view, int position) { Notification notification = adapter.getItem(position); super.more(notification.getStatus(), view, adapter, position); } - @Override public void onViewMedia(String url, Status.MediaAttachment.Type type) { super.viewMedia(url, type); } + + public void onViewThread(int position) { + Notification notification = adapter.getItem(position); + super.viewThread(notification.getStatus()); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/SFragment.java new file mode 100644 index 00000000..230e73bd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/SFragment.java @@ -0,0 +1,247 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v7.widget.PopupMenu; +import android.support.v7.widget.RecyclerView; +import android.view.MenuItem; +import android.view.View; + +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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an + * awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature + * of that is complicated by how they're coupled with Status and Notification and the corresponding + * adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also + * overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear + * up what needs to be where. */ +public class SFragment extends Fragment { + protected String domain; + protected String accessToken; + protected String loggedInAccountId; + protected String loggedInUsername; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + SharedPreferences preferences = getContext().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); + + sendUserInfoRequest(); + } + + protected 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); + } + + protected void postRequest(String endpoint) { + sendRequest(Request.Method.POST, endpoint, null, null); + } + + private void sendUserInfoRequest() { + sendRequest(Request.Method.GET, getString(R.string.endpoint_verify_credentials), null, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + try { + loggedInAccountId = response.getString("id"); + loggedInUsername = response.getString("acct"); + } catch (JSONException e) { + //TODO: Help + assert(false); + } + } + }); + } + + protected void reply(Status status) { + String inReplyToId = status.getId(); + Status.Mention[] mentions = status.getMentions(); + List mentionedUsernames = new ArrayList<>(); + for (int i = 0; i < mentions.length; i++) { + mentionedUsernames.add(mentions[i].getUsername()); + } + mentionedUsernames.add(status.getUsername()); + mentionedUsernames.remove(loggedInUsername); + Intent intent = new Intent(getContext(), ComposeActivity.class); + intent.putExtra("in_reply_to_id", inReplyToId); + intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0])); + startActivity(intent); + } + + protected void reblog(final Status status, final boolean reblog, + final RecyclerView.Adapter adapter, final int position) { + String id = status.getId(); + String 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); + } + }); + } + + protected void favourite(final Status status, final boolean favourite, + final RecyclerView.Adapter adapter, final int 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); + } + + protected void more(Status status, View view, final AdapterItemRemover adapter, + final int 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 (loggedInAccountId == null || !loggedInAccountId.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(); + } + + protected void viewMedia(String url, Status.MediaAttachment.Type type) { + switch (type) { + case IMAGE: { + Fragment newFragment = ViewMediaFragment.newInstance(url); + FragmentManager manager = getFragmentManager(); + manager.beginTransaction() + .add(R.id.overlay_fragment_container, newFragment) + .addToBackStack(null) + .commit(); + break; + } + case VIDEO: { + Intent intent = new Intent(getContext(), ViewVideoActivity.class); + intent.putExtra("url", url); + startActivity(intent); + break; + } + } + } + + protected void viewThread(Status status) { + Intent intent = new Intent(getContext(), ViewThreadActivity.class); + intent.putExtra("id", status.getId()); + startActivity(intent); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java index 7068bd6a..85852384 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/StatusActionListener.java @@ -23,4 +23,5 @@ public interface StatusActionListener { void onFavourite(final boolean favourite, final int position); void onMore(View view, final int position); void onViewMedia(String url, Status.MediaAttachment.Type type); + void onViewThread(int position); } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java new file mode 100644 index 00000000..49737166 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java @@ -0,0 +1,266 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.text.Spanned; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.NetworkImageView; + +import java.util.Date; + +public class StatusViewHolder extends RecyclerView.ViewHolder { + private View container; + private TextView displayName; + private TextView username; + private TextView sinceCreated; + private TextView content; + 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; + private NetworkImageView mediaPreview0; + private NetworkImageView mediaPreview1; + private NetworkImageView mediaPreview2; + private NetworkImageView mediaPreview3; + private View sensitiveMediaWarning; + + public StatusViewHolder(View itemView) { + super(itemView); + container = itemView.findViewById(R.id.status_container); + displayName = (TextView) itemView.findViewById(R.id.status_display_name); + username = (TextView) itemView.findViewById(R.id.status_username); + sinceCreated = (TextView) itemView.findViewById(R.id.status_since_created); + content = (TextView) itemView.findViewById(R.id.status_content); + 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); + 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; + mediaPreview0 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_0); + mediaPreview1 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_1); + mediaPreview2 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_2); + mediaPreview3 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_3); + mediaPreview0.setDefaultImageResId(R.drawable.media_preview_unloaded); + mediaPreview1.setDefaultImageResId(R.drawable.media_preview_unloaded); + mediaPreview2.setDefaultImageResId(R.drawable.media_preview_unloaded); + mediaPreview3.setDefaultImageResId(R.drawable.media_preview_unloaded); + sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning); + } + + public void setDisplayName(String name) { + displayName.setText(name); + } + + public void setUsername(String name) { + Context context = username.getContext(); + String format = context.getString(R.string.status_username_format); + String usernameText = String.format(format, name); + username.setText(usernameText); + } + + public void setContent(Spanned content) { + this.content.setText(content); + } + + public void setAvatar(String url) { + Context context = avatar.getContext(); + ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader(); + avatar.setImageUrl(url, imageLoader); + avatar.setDefaultImageResId(R.drawable.avatar_default); + avatar.setErrorImageResId(R.drawable.avatar_error); + } + + public void setCreatedAt(@Nullable Date createdAt) { + String readout; + if (createdAt != null) { + long then = createdAt.getTime(); + long now = new Date().getTime(); + readout = DateUtils.getRelativeTimeSpanString(then, now); + } else { + readout = "?m"; // unknown minutes~ + } + sinceCreated.setText(readout); + } + + public void setRebloggedByUsername(String name) { + Context context = boostedByUsername.getContext(); + String format = context.getString(R.string.status_boosted_format); + String boostedText = String.format(format, name); + boostedByUsername.setText(boostedText); + boostedIcon.setVisibility(View.VISIBLE); + boostedByUsername.setVisibility(View.VISIBLE); + } + + 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 setMediaPreviews(final Status.MediaAttachment[] attachments, + boolean sensitive, final StatusActionListener listener) { + final NetworkImageView[] previews = { + mediaPreview0, + mediaPreview1, + mediaPreview2, + mediaPreview3 + }; + Context context = mediaPreview0.getContext(); + ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader(); + final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); + for (int i = 0; i < n; i++) { + String previewUrl = attachments[i].getPreviewUrl(); + previews[i].setImageUrl(previewUrl, imageLoader); + if (!sensitive) { + previews[i].setVisibility(View.VISIBLE); + } else { + previews[i].setVisibility(View.GONE); + } + final String url = attachments[i].getUrl(); + final Status.MediaAttachment.Type type = attachments[i].getType(); + previews[i].setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewMedia(url, type); + } + }); + } + if (sensitive) { + sensitiveMediaWarning.setVisibility(View.VISIBLE); + sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + v.setVisibility(View.GONE); + for (int i = 0; i < n; i++) { + previews[i].setVisibility(View.VISIBLE); + } + v.setOnClickListener(null); + } + }); + } + // Hide any of the placeholder previews beyond the ones set. + for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { + previews[i].setImageUrl(null, imageLoader); + previews[i].setVisibility(View.GONE); + } + } + + public void hideSensitiveMediaWarning() { + sensitiveMediaWarning.setVisibility(View.GONE); + } + + public void setupButtons(final StatusActionListener listener, final int position) { + replyButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onReply(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); + } + }); + container.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewThread(position); + } + }); + } + + public void setupWithStatus(Status status, StatusActionListener listener, int position) { + setDisplayName(status.getDisplayName()); + setUsername(status.getUsername()); + setCreatedAt(status.getCreatedAt()); + setContent(status.getContent()); + setAvatar(status.getAvatar()); + setContent(status.getContent()); + setReblogged(status.getReblogged()); + setFavourited(status.getFavourited()); + String rebloggedByUsername = status.getRebloggedByUsername(); + if (rebloggedByUsername == null) { + hideRebloggedByUsername(); + } else { + setRebloggedByUsername(rebloggedByUsername); + } + Status.MediaAttachment[] attachments = status.getAttachments(); + boolean sensitive = status.getSensitive(); + setMediaPreviews(attachments, sensitive, listener); + /* A status without attachments is sometimes still marked sensitive, so it's necessary to + * check both whether there are any attachments and if it's marked sensitive. */ + if (!sensitive || attachments.length == 0) { + hideSensitiveMediaWarning(); + } + setupButtons(listener, position); + if (status.getVisibility() == Status.Visibility.PRIVATE) { + disableReblogging(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java b/app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java new file mode 100644 index 00000000..cbbd7e48 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java @@ -0,0 +1,83 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +public class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover { + private List statuses; + private StatusActionListener statusActionListener; + private int statusIndex; + + public ThreadAdapter(StatusActionListener listener) { + this.statusActionListener = listener; + this.statuses = new ArrayList<>(); + this.statusIndex = 0; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_status, parent, false); + return new StatusViewHolder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + StatusViewHolder holder = (StatusViewHolder) viewHolder; + Status status = statuses.get(position); + holder.setupWithStatus(status, statusActionListener, position); + } + + @Override + public int getItemCount() { + return statuses.size(); + } + + public Status getItem(int position) { + return statuses.get(position); + } + + public void removeItem(int position) { + statuses.remove(position); + notifyItemRemoved(position); + } + + public int insertStatus(Status status) { + int i = statusIndex; + statuses.add(i, status); + notifyItemInserted(i); + return i; + } + + public void addAncestors(List ancestors) { + statusIndex = ancestors.size(); + statuses.addAll(0, ancestors); + notifyItemRangeInserted(0, statusIndex); + } + + public void addDescendants(List descendants) { + int end = statuses.size(); + statuses.addAll(descendants); + notifyItemRangeInserted(end, descendants.size()); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java index ea59b961..a8323af9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java @@ -15,30 +15,16 @@ package com.keylesspalace.tusky; -import android.content.Context; import android.support.annotation.Nullable; -import android.support.v7.widget.PagerSnapHelper; import android.support.v7.widget.RecyclerView; -import android.text.Spanned; -import android.text.style.ImageSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.android.volley.toolbox.ImageLoader; -import com.android.volley.toolbox.NetworkImageView; import java.util.ArrayList; -import java.util.Date; import java.util.List; -public class TimelineAdapter extends RecyclerView.Adapter { +public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover { private static final int VIEW_TYPE_STATUS = 0; private static final int VIEW_TYPE_FOOTER = 1; @@ -76,32 +62,7 @@ public class TimelineAdapter extends RecyclerView.Adapter { if (position < statuses.size()) { StatusViewHolder holder = (StatusViewHolder) viewHolder; Status status = statuses.get(position); - holder.setDisplayName(status.getDisplayName()); - holder.setUsername(status.getUsername()); - holder.setCreatedAt(status.getCreatedAt()); - 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.hideRebloggedByUsername(); - } else { - holder.setRebloggedByUsername(rebloggedByUsername); - } - Status.MediaAttachment[] attachments = status.getAttachments(); - boolean sensitive = status.getSensitive(); - holder.setMediaPreviews(attachments, sensitive, statusListener); - /* A status without attachments is sometimes still marked sensitive, so it's necessary - * to check both whether there are any attachments and if it's marked sensitive. */ - if (!sensitive || attachments.length == 0) { - holder.hideSensitiveMediaWarning(); - } - holder.setupButtons(statusListener, position); - if (status.getVisibility() == Status.Visibility.PRIVATE) { - holder.disableReblogging(); - } + holder.setupWithStatus(status, statusListener, position); } else { FooterViewHolder holder = (FooterViewHolder) viewHolder; holder.setupButton(footerListener); @@ -158,267 +119,4 @@ public class TimelineAdapter extends RecyclerView.Adapter { } return null; } - - public static class StatusViewHolder extends RecyclerView.ViewHolder { - private TextView displayName; - private TextView username; - private TextView sinceCreated; - private TextView content; - 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; - private NetworkImageView mediaPreview0; - private NetworkImageView mediaPreview1; - private NetworkImageView mediaPreview2; - private NetworkImageView mediaPreview3; - private View sensitiveMediaWarning; - - public StatusViewHolder(View itemView) { - super(itemView); - displayName = (TextView) itemView.findViewById(R.id.status_display_name); - username = (TextView) itemView.findViewById(R.id.status_username); - sinceCreated = (TextView) itemView.findViewById(R.id.status_since_created); - content = (TextView) itemView.findViewById(R.id.status_content); - 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); - 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; - mediaPreview0 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_0); - mediaPreview1 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_1); - mediaPreview2 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_2); - mediaPreview3 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_3); - mediaPreview0.setDefaultImageResId(R.drawable.media_preview_unloaded); - mediaPreview1.setDefaultImageResId(R.drawable.media_preview_unloaded); - mediaPreview2.setDefaultImageResId(R.drawable.media_preview_unloaded); - mediaPreview3.setDefaultImageResId(R.drawable.media_preview_unloaded); - sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning); - } - - public void setDisplayName(String name) { - displayName.setText(name); - } - - public void setUsername(String name) { - Context context = username.getContext(); - String format = context.getString(R.string.status_username_format); - String usernameText = String.format(format, name); - username.setText(usernameText); - } - - public void setContent(Spanned content) { - this.content.setText(content); - } - - public void setAvatar(String url) { - Context context = avatar.getContext(); - ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader(); - avatar.setImageUrl(url, imageLoader); - avatar.setDefaultImageResId(R.drawable.avatar_default); - avatar.setErrorImageResId(R.drawable.avatar_error); - } - - /* This is a rough duplicate of android.text.format.DateUtils.getRelativeTimeSpanString, - * but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough. */ - private String getRelativeTimeSpanString(long then, long now) { - final long MINUTE = 60; - final long HOUR = 60 * MINUTE; - final long DAY = 24 * HOUR; - final long YEAR = 365 * DAY; - long span = (now - then) / 1000; - String prefix = ""; - if (span < 0) { - prefix = "in "; - span = -span; - } - String unit; - if (span < MINUTE) { - unit = "s"; - } else if (span < HOUR) { - span /= MINUTE; - unit = "m"; - } else if (span < DAY) { - span /= HOUR; - unit = "h"; - } else if (span < YEAR) { - span /= DAY; - unit = "d"; - } else { - span /= YEAR; - unit = "y"; - } - return prefix + span + unit; - } - - public void setCreatedAt(@Nullable Date createdAt) { - String readout; - if (createdAt != null) { - long then = createdAt.getTime(); - long now = new Date().getTime(); - readout = getRelativeTimeSpanString(then, now); - } else { - readout = "?m"; // unknown minutes~ - } - sinceCreated.setText(readout); - } - - public void setRebloggedByUsername(String name) { - Context context = boostedByUsername.getContext(); - String format = context.getString(R.string.status_boosted_format); - String boostedText = String.format(format, name); - boostedByUsername.setText(boostedText); - boostedIcon.setVisibility(View.VISIBLE); - boostedByUsername.setVisibility(View.VISIBLE); - } - - 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 setMediaPreviews(final Status.MediaAttachment[] attachments, - boolean sensitive, final StatusActionListener listener) { - final NetworkImageView[] previews = { - mediaPreview0, - mediaPreview1, - mediaPreview2, - mediaPreview3 - }; - Context context = mediaPreview0.getContext(); - ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader(); - final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS); - for (int i = 0; i < n; i++) { - String previewUrl = attachments[i].getPreviewUrl(); - previews[i].setImageUrl(previewUrl, imageLoader); - if (!sensitive) { - previews[i].setVisibility(View.VISIBLE); - } else { - previews[i].setVisibility(View.GONE); - } - final String url = attachments[i].getUrl(); - final Status.MediaAttachment.Type type = attachments[i].getType(); - previews[i].setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onViewMedia(url, type); - } - }); - } - if (sensitive) { - sensitiveMediaWarning.setVisibility(View.VISIBLE); - sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - v.setVisibility(View.GONE); - for (int i = 0; i < n; i++) { - previews[i].setVisibility(View.VISIBLE); - } - v.setOnClickListener(null); - } - }); - } - // Hide any of the placeholder previews beyond the ones set. - for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { - previews[i].setImageUrl(null, imageLoader); - previews[i].setVisibility(View.GONE); - } - } - - public void hideSensitiveMediaWarning() { - sensitiveMediaWarning.setVisibility(View.GONE); - } - - public void setupButtons(final StatusActionListener listener, final int position) { - replyButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onReply(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); - } - }); - } - } - - public static class FooterViewHolder extends RecyclerView.ViewHolder { - private LinearLayout retryBar; - private Button retry; - private ProgressBar progressBar; - - public FooterViewHolder(View itemView) { - super(itemView); - retryBar = (LinearLayout) itemView.findViewById(R.id.footer_retry_bar); - retry = (Button) itemView.findViewById(R.id.footer_retry_button); - progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar); - progressBar.setIndeterminate(true); - } - - public void setupButton(final FooterActionListener listener) { - retry.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onLoadMore(); - } - }); - } - - public void showRetry(boolean show) { - if (!show) { - retryBar.setVisibility(View.GONE); - progressBar.setVisibility(View.VISIBLE); - } else { - retryBar.setVisibility(View.VISIBLE); - progressBar.setVisibility(View.GONE); - } - } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java index 3c8991ee..d3958dda 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/TimelineFragment.java @@ -16,42 +16,31 @@ package com.keylesspalace.tusky; import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.support.annotation.Nullable; import android.support.design.widget.TabLayout; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; 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 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.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -public class TimelineFragment extends Fragment implements +public class TimelineFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener { public enum Kind { @@ -60,12 +49,6 @@ public class TimelineFragment extends Fragment implements PUBLIC, } - private String domain = null; - private String accessToken = null; - /** ID of the account that is currently logged-in. */ - private String userAccountId = null; - /** Username of the account that is currently logged-in. */ - private String userUsername = null; private SwipeRefreshLayout swipeRefreshLayout; private RecyclerView recyclerView; private TimelineAdapter adapter; @@ -90,15 +73,8 @@ public class TimelineFragment extends Fragment implements 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. + Context context = getContext(); swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout); swipeRefreshLayout.setOnRefreshListener(this); // Setup the RecyclerView. @@ -121,7 +97,6 @@ public class TimelineFragment extends Fragment implements } else { sendFetchTimelineRequest(); } - } }; recyclerView.addOnScrollListener(scrollListener); @@ -143,7 +118,6 @@ public class TimelineFragment extends Fragment implements }; layout.addOnTabSelectedListener(onTabSelectedListener); - sendUserInfoRequest(); sendFetchTimelineRequest(); return rootView; @@ -161,22 +135,6 @@ public class TimelineFragment extends Fragment implements scrollListener.reset(); } - 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"); - userUsername = response.getString("acct"); - } catch (JSONException e) { - //TODO: Help - assert(false); - } - } - }); - } - private void sendFetchTimelineRequest(final String fromId) { String endpoint; switch (kind) { @@ -251,7 +209,7 @@ public class TimelineFragment extends Fragment implements RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1); if (viewHolder != null) { - TimelineAdapter.FooterViewHolder holder = (TimelineAdapter.FooterViewHolder) viewHolder; + FooterViewHolder holder = (FooterViewHolder) viewHolder; holder.showRetry(show); } } @@ -260,163 +218,6 @@ public class TimelineFragment extends Fragment implements 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 onReply(int position) { - Status status = adapter.getItem(position); - String inReplyToId = status.getId(); - Status.Mention[] mentions = status.getMentions(); - List mentionedUsernames = new ArrayList<>(); - for (int i = 0; i < mentions.length; i++) { - mentionedUsernames.add(mentions[i].getUsername()); - } - mentionedUsernames.add(status.getUsername()); - mentionedUsernames.remove(userUsername); - Intent intent = new Intent(getContext(), ComposeActivity.class); - intent.putExtra("in_reply_to_id", inReplyToId); - intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0])); - startActivity(intent); - } - - 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(); - } - - public void onViewMedia(String url, Status.MediaAttachment.Type type) { - switch (type) { - case IMAGE: { - Fragment newFragment = ViewMediaFragment.newInstance(url); - FragmentManager manager = getFragmentManager(); - manager.beginTransaction() - .add(R.id.overlay_fragment_container, newFragment) - .addToBackStack(null) - .commit(); - break; - } - case VIDEO: { - Intent intent = new Intent(getContext(), ViewVideoActivity.class); - intent.putExtra("url", url); - startActivity(intent); - break; - } - } - } - public void onLoadMore() { Status status = adapter.getItem(adapter.getItemCount() - 2); if (status != null) { @@ -425,4 +226,28 @@ public class TimelineFragment extends Fragment implements sendFetchTimelineRequest(); } } + + public void onReply(int position) { + super.reply(adapter.getItem(position)); + } + + public void onReblog(final boolean reblog, final int position) { + super.reblog(adapter.getItem(position), reblog, adapter, position); + } + + public void onFavourite(final boolean favourite, final int position) { + super.favourite(adapter.getItem(position), favourite, adapter, position); + } + + public void onMore(View view, final int position) { + super.more(adapter.getItem(position), view, adapter, position); + } + + public void onViewMedia(String url, Status.MediaAttachment.Type type) { + super.viewMedia(url, type); + } + + public void onViewThread(int position) { + super.viewThread(adapter.getItem(position)); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java new file mode 100644 index 00000000..c6e31a8e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java @@ -0,0 +1,68 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; + +public class ViewThreadActivity extends AppCompatActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_thread); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setTitle(R.string.title_thread); + } + + String id = getIntent().getStringExtra("id"); + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + Fragment fragment = ViewThreadFragment.newInstance(id); + fragmentTransaction.add(R.id.fragment_container, fragment); + fragmentTransaction.commit(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.view_thread_toolbar, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_back: { + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + } + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java new file mode 100644 index 00000000..9dbfd61a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadFragment.java @@ -0,0 +1,143 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky. If not, see + * . */ + +package com.keylesspalace.tusky; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +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 com.android.volley.Request; +import com.android.volley.Response; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; + +public class ViewThreadFragment extends SFragment implements StatusActionListener { + private RecyclerView recyclerView; + private ThreadAdapter adapter; + + public static ViewThreadFragment newInstance(String id) { + Bundle arguments = new Bundle(); + ViewThreadFragment fragment = new ViewThreadFragment(); + arguments.putString("id", id); + 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_view_thread, container, false); + + Context context = getContext(); + 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); + adapter = new ThreadAdapter(this); + recyclerView.setAdapter(adapter); + + String id = getArguments().getString("id"); + sendStatusRequest(id); + sendThreadRequest(id); + + return rootView; + } + + private void sendStatusRequest(String id) { + String endpoint = String.format(getString(R.string.endpoint_get_status), id); + super.sendRequest(Request.Method.GET, endpoint, null, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + Status status; + try { + status = Status.parse(response, false); + } catch (JSONException e) { + onThreadRequestFailure(); + return; + } + int position = adapter.insertStatus(status); + recyclerView.scrollToPosition(position); + } + }); + } + + private void sendThreadRequest(String id) { + String endpoint = String.format(getString(R.string.endpoint_context), id); + super.sendRequest(Request.Method.GET, endpoint, null, + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + try { + List ancestors = + Status.parse(response.getJSONArray("ancestors")); + List descendants = + Status.parse(response.getJSONArray("descendants")); + adapter.addAncestors(ancestors); + adapter.addDescendants(descendants); + } catch (JSONException e) { + onThreadRequestFailure(); + } + } + }); + } + + private void onThreadRequestFailure() { + //TODO: no + assert(false); + } + + public void onReply(int position) { + super.reply(adapter.getItem(position)); + } + + public void onReblog(boolean reblog, int position) { + super.reblog(adapter.getItem(position), reblog, adapter, position); + } + + public void onFavourite(boolean favourite, int position) { + super.favourite(adapter.getItem(position), favourite, adapter, position); + } + + public void onMore(View view, int position) { + super.more(adapter.getItem(position), view, adapter, position); + } + + public void onViewMedia(String url, Status.MediaAttachment.Type type) { + super.viewMedia(url, type); + } + + public void onViewThread(int position) { + super.viewThread(adapter.getItem(position)); + } +} diff --git a/app/src/main/res/drawable/boost_icon.png b/app/src/main/res/drawable/boost_icon.png deleted file mode 100644 index 6542771244f367e646abe3adc4579038c6172eaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 221 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~g!3HGv?z=Y!NO2Z;L>4nJa0`PlBg3pY5H=O_6Iz&%o;{o_a_Mfg(OQ{BTAg}b8}PkN*J7rQWHy3QxwWGOEMJPJ$(bh8~Mb6 zio!iz978nDAD!gMb-;j!* + + diff --git a/app/src/main/res/drawable/ic_favourited.xml b/app/src/main/res/drawable/ic_favourited.xml new file mode 100644 index 00000000..62dc1101 --- /dev/null +++ b/app/src/main/res/drawable/ic_favourited.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_followed.xml b/app/src/main/res/drawable/ic_followed.xml new file mode 100644 index 00000000..a586267b --- /dev/null +++ b/app/src/main/res/drawable/ic_followed.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_reblogged.xml b/app/src/main/res/drawable/ic_reblogged.xml new file mode 100644 index 00000000..18fcfcd6 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblogged.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/activity_view_thread.xml b/app/src/main/res/layout/activity_view_thread.xml new file mode 100644 index 00000000..f9f4c85b --- /dev/null +++ b/app/src/main/res/layout/activity_view_thread.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_view_thread.xml b/app/src/main/res/layout/fragment_view_thread.xml new file mode 100644 index 00000000..732fbb63 --- /dev/null +++ b/app/src/main/res/layout/fragment_view_thread.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_follow.xml b/app/src/main/res/layout/item_follow.xml new file mode 100644 index 00000000..6bebfeb8 --- /dev/null +++ b/app/src/main/res/layout/item_follow.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification.xml b/app/src/main/res/layout/item_notification.xml deleted file mode 100644 index f0c40d8a..00000000 --- a/app/src/main/res/layout/item_notification.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index a541b52e..dd1c538d 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -1,9 +1,9 @@ + android:layout_height="wrap_content" + android:id="@+id/status_container"> + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/view_thread_toolbar.xml b/app/src/main/res/menu/view_thread_toolbar.xml new file mode 100644 index 00000000..0f352f24 --- /dev/null +++ b/app/src/main/res/menu/view_thread_toolbar.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1f98e43a..1f805ccb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -8,4 +8,5 @@ #303030 #DFDFDF #4F5F6F + #9F9F9F diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 42e27bc9..2742ab6f 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -3,6 +3,7 @@ 0dp 4dp 4dp + 56dp 8dp 5dp 4dp @@ -12,4 +13,5 @@ 16dp 48dp 8dp + 4dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01d04759..802dc566 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,6 +52,7 @@ Home Notifications Public + Thread \@%s %s boosted @@ -60,8 +61,8 @@ Could not load the rest of the toots. - %s boosted your status - %s favourited your status + %s boosted your toot + %s favourited your toot %s followed you Compose @@ -74,6 +75,9 @@ Retry Mark Sensitive Cancel + Back + + Toot! Domain What\'s Happening?