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 65427712..00000000
Binary files a/app/src/main/res/drawable/boost_icon.png and /dev/null differ
diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml
new file mode 100644
index 00000000..cceab543
--- /dev/null
+++ b/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,7 @@
+
+
+
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?