Adds a toot thread viewing mode. Also, many files were missing and didn't push so the previous commits may have been very wrong?
This commit is contained in:
parent
32fecabd7f
commit
b00a3cf443
31 changed files with 1274 additions and 566 deletions
|
@ -32,6 +32,7 @@
|
||||||
android:name=".ComposeActivity"
|
android:name=".ComposeActivity"
|
||||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||||
<activity android:name=".ViewVideoActivity" />
|
<activity android:name=".ViewVideoActivity" />
|
||||||
|
<activity android:name=".ViewThreadActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
|
public interface AdapterItemRemover {
|
||||||
|
void removeItem(int position);
|
||||||
|
}
|
|
@ -323,7 +323,7 @@ public class ComposeActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSendSuccess() {
|
private void onSendSuccess() {
|
||||||
Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, getString(R.string.confirmation_send), Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
50
app/src/main/java/com/keylesspalace/tusky/DateUtils.java
Normal file
50
app/src/main/java/com/keylesspalace/tusky/DateUtils.java
Normal file
|
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,4 +55,10 @@ public class Notification {
|
||||||
public void setStatus(Status status) {
|
public void setStatus(Status status) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasStatusType() {
|
||||||
|
return type == Type.MENTION
|
||||||
|
|| type == Type.FAVOURITE
|
||||||
|
|| type == Type.REBLOG;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,39 +16,128 @@
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.text.Spanned;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class NotificationsAdapter extends RecyclerView.Adapter {
|
public class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||||
private List<Notification> notifications = new ArrayList<>();
|
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<Notification> notifications;
|
||||||
|
private StatusActionListener statusListener;
|
||||||
|
private FooterActionListener footerListener;
|
||||||
|
|
||||||
|
public NotificationsAdapter(StatusActionListener statusListener,
|
||||||
|
FooterActionListener footerListener) {
|
||||||
|
super();
|
||||||
|
notifications = new ArrayList<>();
|
||||||
|
this.statusListener = statusListener;
|
||||||
|
this.footerListener = footerListener;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
switch (viewType) {
|
||||||
.inflate(R.layout.item_notification, parent, false);
|
default:
|
||||||
return new ViewHolder(view);
|
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
|
@Override
|
||||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
ViewHolder holder = (ViewHolder) viewHolder;
|
if (position < notifications.size()) {
|
||||||
Notification notification = notifications.get(position);
|
Notification notification = notifications.get(position);
|
||||||
holder.setMessage(notification.getType(), notification.getDisplayName());
|
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
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
return notifications.size();
|
return notifications.size() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Notification getItem(int position) {
|
@Override
|
||||||
return notifications.get(position);
|
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<Notification> new_notifications) {
|
public int update(List<Notification> new_notifications) {
|
||||||
|
@ -76,39 +165,62 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
notifyItemRangeInserted(end, new_notifications.size());
|
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;
|
private TextView message;
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
public FollowViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
message = (TextView) itemView.findViewById(R.id.notification_text);
|
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();
|
Context context = message.getContext();
|
||||||
String wholeMessage = "";
|
String format = context.getString(R.string.notification_follow_format);
|
||||||
switch (type) {
|
String wholeMessage = String.format(format, displayName);
|
||||||
case MENTION: {
|
|
||||||
wholeMessage = displayName + " mentioned you";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case REBLOG: {
|
|
||||||
String format = context.getString(R.string.notification_reblog_format);
|
|
||||||
wholeMessage = String.format(format, displayName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case FAVOURITE: {
|
|
||||||
String format = context.getString(R.string.notification_favourite_format);
|
|
||||||
wholeMessage = String.format(format, displayName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case FOLLOW: {
|
|
||||||
String format = context.getString(R.string.notification_follow_format);
|
|
||||||
wholeMessage = String.format(format, displayName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
message.setText(wholeMessage);
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,12 +172,10 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRefresh() {
|
public void onRefresh() {
|
||||||
sendFetchNotificationsRequest();
|
sendFetchNotificationsRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadMore() {
|
public void onLoadMore() {
|
||||||
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
||||||
if (notification != null) {
|
if (notification != null) {
|
||||||
|
@ -187,32 +185,32 @@ public class NotificationsFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReply(int position) {
|
public void onReply(int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = adapter.getItem(position);
|
||||||
super.reply(notification.getStatus());
|
super.reply(notification.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReblog(boolean reblog, int position) {
|
public void onReblog(boolean reblog, int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = adapter.getItem(position);
|
||||||
super.reblog(notification.getStatus(), reblog, adapter, position);
|
super.reblog(notification.getStatus(), reblog, adapter, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFavourite(boolean favourite, int position) {
|
public void onFavourite(boolean favourite, int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = adapter.getItem(position);
|
||||||
super.favourite(notification.getStatus(), favourite, adapter, position);
|
super.favourite(notification.getStatus(), favourite, adapter, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMore(View view, int position) {
|
public void onMore(View view, int position) {
|
||||||
Notification notification = adapter.getItem(position);
|
Notification notification = adapter.getItem(position);
|
||||||
super.more(notification.getStatus(), view, adapter, position);
|
super.more(notification.getStatus(), view, adapter, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewMedia(String url, Status.MediaAttachment.Type type) {
|
public void onViewMedia(String url, Status.MediaAttachment.Type type) {
|
||||||
super.viewMedia(url, type);
|
super.viewMedia(url, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onViewThread(int position) {
|
||||||
|
Notification notification = adapter.getItem(position);
|
||||||
|
super.viewThread(notification.getStatus());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
247
app/src/main/java/com/keylesspalace/tusky/SFragment.java
Normal file
247
app/src/main/java/com/keylesspalace/tusky/SFragment.java
Normal file
|
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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<JSONObject> responseListener) {
|
||||||
|
if (responseListener == null) {
|
||||||
|
// Use a dummy listener if one wasn't specified so the request can be constructed.
|
||||||
|
responseListener = new Response.Listener<JSONObject>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(JSONObject response) {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
String url = "https://" + domain + endpoint;
|
||||||
|
JsonObjectRequest request = new JsonObjectRequest(
|
||||||
|
method, url, parameters, responseListener,
|
||||||
|
new Response.ErrorListener() {
|
||||||
|
@Override
|
||||||
|
public void onErrorResponse(VolleyError error) {
|
||||||
|
System.err.println(error.getMessage());
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
headers.put("Authorization", "Bearer " + accessToken);
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<JSONObject>() {
|
||||||
|
@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<String> 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<JSONObject>() {
|
||||||
|
@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<JSONObject>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(JSONObject response) {
|
||||||
|
status.setFavourited(favourite);
|
||||||
|
adapter.notifyItemChanged(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void follow(String id) {
|
||||||
|
String endpoint = String.format(getString(R.string.endpoint_follow), id);
|
||||||
|
postRequest(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void block(String id) {
|
||||||
|
String endpoint = String.format(getString(R.string.endpoint_block), id);
|
||||||
|
postRequest(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void delete(String id) {
|
||||||
|
String endpoint = String.format(getString(R.string.endpoint_delete), id);
|
||||||
|
sendRequest(Request.Method.DELETE, endpoint, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,4 +23,5 @@ public interface StatusActionListener {
|
||||||
void onFavourite(final boolean favourite, final int position);
|
void onFavourite(final boolean favourite, final int position);
|
||||||
void onMore(View view, final int position);
|
void onMore(View view, final int position);
|
||||||
void onViewMedia(String url, Status.MediaAttachment.Type type);
|
void onViewMedia(String url, Status.MediaAttachment.Type type);
|
||||||
|
void onViewThread(int position);
|
||||||
}
|
}
|
||||||
|
|
266
app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java
Normal file
266
app/src/main/java/com/keylesspalace/tusky/StatusViewHolder.java
Normal file
|
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
83
app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java
Normal file
83
app/src/main/java/com/keylesspalace/tusky/ThreadAdapter.java
Normal file
|
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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<Status> 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<Status> ancestors) {
|
||||||
|
statusIndex = ancestors.size();
|
||||||
|
statuses.addAll(0, ancestors);
|
||||||
|
notifyItemRangeInserted(0, statusIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDescendants(List<Status> descendants) {
|
||||||
|
int end = statuses.size();
|
||||||
|
statuses.addAll(descendants);
|
||||||
|
notifyItemRangeInserted(end, descendants.size());
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,30 +15,16 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.widget.PagerSnapHelper;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.style.ImageSpan;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
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.ArrayList;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
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_STATUS = 0;
|
||||||
private static final int VIEW_TYPE_FOOTER = 1;
|
private static final int VIEW_TYPE_FOOTER = 1;
|
||||||
|
|
||||||
|
@ -76,32 +62,7 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
||||||
if (position < statuses.size()) {
|
if (position < statuses.size()) {
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||||
Status status = statuses.get(position);
|
Status status = statuses.get(position);
|
||||||
holder.setDisplayName(status.getDisplayName());
|
holder.setupWithStatus(status, statusListener, position);
|
||||||
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();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||||
holder.setupButton(footerListener);
|
holder.setupButton(footerListener);
|
||||||
|
@ -158,267 +119,4 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
return null;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,42 +16,31 @@
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.design.widget.TabLayout;
|
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.content.ContextCompat;
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
import android.support.v4.widget.SwipeRefreshLayout;
|
||||||
import android.support.v7.widget.DividerItemDecoration;
|
import android.support.v7.widget.DividerItemDecoration;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.PopupMenu;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import com.android.volley.AuthFailureError;
|
import com.android.volley.AuthFailureError;
|
||||||
import com.android.volley.Request;
|
|
||||||
import com.android.volley.Response;
|
import com.android.volley.Response;
|
||||||
import com.android.volley.VolleyError;
|
import com.android.volley.VolleyError;
|
||||||
import com.android.volley.toolbox.JsonArrayRequest;
|
import com.android.volley.toolbox.JsonArrayRequest;
|
||||||
import com.android.volley.toolbox.JsonObjectRequest;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class TimelineFragment extends Fragment implements
|
public class TimelineFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener {
|
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener {
|
||||||
|
|
||||||
public enum Kind {
|
public enum Kind {
|
||||||
|
@ -60,12 +49,6 @@ public class TimelineFragment extends Fragment implements
|
||||||
PUBLIC,
|
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 SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private TimelineAdapter adapter;
|
private TimelineAdapter adapter;
|
||||||
|
@ -90,15 +73,8 @@ public class TimelineFragment extends Fragment implements
|
||||||
|
|
||||||
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
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.
|
// Setup the SwipeRefreshLayout.
|
||||||
|
Context context = getContext();
|
||||||
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
|
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
|
||||||
swipeRefreshLayout.setOnRefreshListener(this);
|
swipeRefreshLayout.setOnRefreshListener(this);
|
||||||
// Setup the RecyclerView.
|
// Setup the RecyclerView.
|
||||||
|
@ -121,7 +97,6 @@ public class TimelineFragment extends Fragment implements
|
||||||
} else {
|
} else {
|
||||||
sendFetchTimelineRequest();
|
sendFetchTimelineRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
recyclerView.addOnScrollListener(scrollListener);
|
recyclerView.addOnScrollListener(scrollListener);
|
||||||
|
@ -143,7 +118,6 @@ public class TimelineFragment extends Fragment implements
|
||||||
};
|
};
|
||||||
layout.addOnTabSelectedListener(onTabSelectedListener);
|
layout.addOnTabSelectedListener(onTabSelectedListener);
|
||||||
|
|
||||||
sendUserInfoRequest();
|
|
||||||
sendFetchTimelineRequest();
|
sendFetchTimelineRequest();
|
||||||
|
|
||||||
return rootView;
|
return rootView;
|
||||||
|
@ -161,22 +135,6 @@ public class TimelineFragment extends Fragment implements
|
||||||
scrollListener.reset();
|
scrollListener.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendUserInfoRequest() {
|
|
||||||
sendRequest(Request.Method.GET, getString(R.string.endpoint_verify_credentials), null,
|
|
||||||
new Response.Listener<JSONObject>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(JSONObject response) {
|
|
||||||
try {
|
|
||||||
userAccountId = response.getString("id");
|
|
||||||
userUsername = response.getString("acct");
|
|
||||||
} catch (JSONException e) {
|
|
||||||
//TODO: Help
|
|
||||||
assert(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendFetchTimelineRequest(final String fromId) {
|
private void sendFetchTimelineRequest(final String fromId) {
|
||||||
String endpoint;
|
String endpoint;
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
|
@ -251,7 +209,7 @@ public class TimelineFragment extends Fragment implements
|
||||||
RecyclerView.ViewHolder viewHolder =
|
RecyclerView.ViewHolder viewHolder =
|
||||||
recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1);
|
recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1);
|
||||||
if (viewHolder != null) {
|
if (viewHolder != null) {
|
||||||
TimelineAdapter.FooterViewHolder holder = (TimelineAdapter.FooterViewHolder) viewHolder;
|
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||||
holder.showRetry(show);
|
holder.showRetry(show);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,163 +218,6 @@ public class TimelineFragment extends Fragment implements
|
||||||
sendFetchTimelineRequest();
|
sendFetchTimelineRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendRequest(
|
|
||||||
int method, String endpoint, JSONObject parameters,
|
|
||||||
@Nullable Response.Listener<JSONObject> responseListener) {
|
|
||||||
if (responseListener == null) {
|
|
||||||
// Use a dummy listener if one wasn't specified so the request can be constructed.
|
|
||||||
responseListener = new Response.Listener<JSONObject>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(JSONObject response) {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
String url = "https://" + domain + endpoint;
|
|
||||||
JsonObjectRequest request = new JsonObjectRequest(
|
|
||||||
method, url, parameters, responseListener,
|
|
||||||
new Response.ErrorListener() {
|
|
||||||
@Override
|
|
||||||
public void onErrorResponse(VolleyError error) {
|
|
||||||
System.err.println(error.getMessage());
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
@Override
|
|
||||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
|
||||||
Map<String, String> headers = new HashMap<>();
|
|
||||||
headers.put("Authorization", "Bearer " + accessToken);
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
VolleySingleton.getInstance(getContext()).addToRequestQueue(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postRequest(String endpoint) {
|
|
||||||
sendRequest(Request.Method.POST, endpoint, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onReply(int position) {
|
|
||||||
Status status = adapter.getItem(position);
|
|
||||||
String inReplyToId = status.getId();
|
|
||||||
Status.Mention[] mentions = status.getMentions();
|
|
||||||
List<String> 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<JSONObject>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(JSONObject response) {
|
|
||||||
status.setReblogged(reblog);
|
|
||||||
adapter.notifyItemChanged(position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onFavourite(final boolean favourite, final int position) {
|
|
||||||
final Status status = adapter.getItem(position);
|
|
||||||
String id = status.getId();
|
|
||||||
String endpoint;
|
|
||||||
if (favourite) {
|
|
||||||
endpoint = String.format(getString(R.string.endpoint_favourite), id);
|
|
||||||
} else {
|
|
||||||
endpoint = String.format(getString(R.string.endpoint_unfavourite), id);
|
|
||||||
}
|
|
||||||
sendRequest(Request.Method.POST, endpoint, null, new Response.Listener<JSONObject>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(JSONObject response) {
|
|
||||||
status.setFavourited(favourite);
|
|
||||||
adapter.notifyItemChanged(position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void follow(String id) {
|
|
||||||
String endpoint = String.format(getString(R.string.endpoint_follow), id);
|
|
||||||
postRequest(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void block(String id) {
|
|
||||||
String endpoint = String.format(getString(R.string.endpoint_block), id);
|
|
||||||
postRequest(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void delete(String id) {
|
|
||||||
String endpoint = String.format(getString(R.string.endpoint_delete), id);
|
|
||||||
sendRequest(Request.Method.DELETE, endpoint, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onMore(View view, final int position) {
|
|
||||||
Status status = adapter.getItem(position);
|
|
||||||
final String id = status.getId();
|
|
||||||
final String accountId = status.getAccountId();
|
|
||||||
PopupMenu popup = new PopupMenu(getContext(), view);
|
|
||||||
// Give a different menu depending on whether this is the user's own toot or not.
|
|
||||||
if (userAccountId == null || !userAccountId.equals(accountId)) {
|
|
||||||
popup.inflate(R.menu.status_more);
|
|
||||||
} else {
|
|
||||||
popup.inflate(R.menu.status_more_for_user);
|
|
||||||
}
|
|
||||||
popup.setOnMenuItemClickListener(
|
|
||||||
new PopupMenu.OnMenuItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.status_follow: {
|
|
||||||
follow(accountId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case R.id.status_block: {
|
|
||||||
block(accountId);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case R.id.status_delete: {
|
|
||||||
delete(id);
|
|
||||||
adapter.removeItem(position);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
popup.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
public void onLoadMore() {
|
||||||
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
||||||
if (status != null) {
|
if (status != null) {
|
||||||
|
@ -425,4 +226,28 @@ public class TimelineFragment extends Fragment implements
|
||||||
sendFetchTimelineRequest();
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
* <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
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<JSONObject>() {
|
||||||
|
@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<JSONObject>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(JSONObject response) {
|
||||||
|
try {
|
||||||
|
List<Status> ancestors =
|
||||||
|
Status.parse(response.getJSONArray("ancestors"));
|
||||||
|
List<Status> 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));
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 221 B |
7
app/src/main/res/drawable/ic_back.xml
Normal file
7
app/src/main/res/drawable/ic_back.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<vector android:height="24dp" android:viewportHeight="850.3937"
|
||||||
|
android:viewportWidth="850.3937" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillAlpha="1" android:fillColor="#ffffff"
|
||||||
|
android:pathData="m410.4,48.8c-9.1,0 -18.1,3.5 -25.1,10.4L84.7,359.9c-1.6,1.6 -2.9,3.2 -4.1,5 -6,6.3 -9.7,14.9 -9.7,24.4l0,70.9c0,11.3 5.2,21.3 13.4,27.8 1.6,3.2 3.8,6.2 6.5,8.9L391.5,797.5c13.9,13.9 36.2,13.9 50.1,0l50.1,-50.1c13.9,-13.9 13.9,-36.2 0,-50.1l-201.7,-201.7 454.1,0c19.6,0 35.4,-15.8 35.4,-35.4l0,-70.9c0,-19.6 -15.8,-35.4 -35.4,-35.4l-452.9,0 194.4,-194.4c13.9,-13.9 13.9,-36.2 0,-50.1l-50.1,-50.1c-6.9,-6.9 -16,-10.4 -25.1,-10.4z"
|
||||||
|
android:strokeAlpha="1" android:strokeColor="#00000000"
|
||||||
|
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="10.62992096"/>
|
||||||
|
</vector>
|
7
app/src/main/res/drawable/ic_favourited.xml
Normal file
7
app/src/main/res/drawable/ic_favourited.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<vector android:height="16dp" android:viewportHeight="566.92914"
|
||||||
|
android:viewportWidth="566.92914" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillAlpha="1" android:fillColor="#000000"
|
||||||
|
android:pathData="M141.7,0C63.2,0 0,63.2 0,141.7L0,425.2C0,503.7 63.2,566.9 141.7,566.9L425.2,566.9C503.7,566.9 566.9,503.7 566.9,425.2L566.9,141.7C566.9,63.2 503.7,0 425.2,0L141.7,0zM283.6,24.8C287.6,24.9 291.2,27.1 293,30.7L363.4,173.4L520.9,196.3C529.6,197.6 533.1,208.3 526.8,214.4L412.8,325.5L439.7,482.3C441.2,491 432.1,497.6 424.3,493.5L283.5,419.5L142.6,493.5C134.8,497.6 125.7,491 127.2,482.3L154.1,325.5L40.2,214.4C33.8,208.3 37.3,197.6 46,196.3L203.5,173.4L273.9,30.7C275.7,27.1 279.5,24.8 283.6,24.8z"
|
||||||
|
android:strokeAlpha="1" android:strokeColor="#9d9d9d"
|
||||||
|
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/>
|
||||||
|
</vector>
|
7
app/src/main/res/drawable/ic_followed.xml
Normal file
7
app/src/main/res/drawable/ic_followed.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<vector android:height="16dp" android:viewportHeight="566.92914"
|
||||||
|
android:viewportWidth="566.92914" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillAlpha="1" android:fillColor="#000000"
|
||||||
|
android:pathData="M141.7,0C63.2,0 0,63.2 0,141.7L0,425.2C0,503.7 63.2,566.9 141.7,566.9L425.2,566.9C503.7,566.9 566.9,503.7 566.9,425.2L566.9,141.7C566.9,63.2 503.7,0 425.2,0L141.7,0zM283.5,70.9A88.6,88.6 0,0 1,372 159.4A88.6,88.6 0,0 1,283.5 248A88.6,88.6 0,0 1,194.9 159.4A88.6,88.6 0,0 1,283.5 70.9zM194.9,311.3C194.9,311.3 229.1,336.6 283.5,336.6C338.4,336.6 370.5,311.3 370.5,311.3C496.1,407.5 460.6,478.3 460.6,478.3L106.3,478.3C106.3,478.3 70.9,407.5 194.9,311.3z"
|
||||||
|
android:strokeAlpha="1" android:strokeColor="#9d9d9d"
|
||||||
|
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/>
|
||||||
|
</vector>
|
7
app/src/main/res/drawable/ic_reblogged.xml
Normal file
7
app/src/main/res/drawable/ic_reblogged.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<vector android:height="16dp" android:viewportHeight="566.92914"
|
||||||
|
android:viewportWidth="566.92914" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillAlpha="1" android:fillColor="#000000"
|
||||||
|
android:pathData="M141.7,0C63.2,0 0,63.2 0,141.7L0,425.2C0,503.7 63.2,566.9 141.7,566.9L425.2,566.9C503.7,566.9 566.9,503.7 566.9,425.2L566.9,141.7C566.9,63.2 503.7,0 425.2,0L141.7,0zM177.2,124L354.3,124C432.9,124 496.1,182.6 496.1,265.7L496.1,336.6L549.2,336.6L460.6,425.2L372,336.6L425.2,336.6L425.2,265.7C425.2,226.4 393.6,194.9 354.3,194.9L248,194.9L177.2,124zM106.3,141.7L194.9,230.3L141.7,230.3L141.7,301.2C141.7,340.5 173.3,372 212.6,372L318.9,372L389.8,442.9L212.6,442.9C134,442.9 70.9,384.3 70.9,301.2L70.9,230.3L17.7,230.3L106.3,141.7z"
|
||||||
|
android:strokeAlpha="1" android:strokeColor="#9d9d9d"
|
||||||
|
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="0"/>
|
||||||
|
</vector>
|
38
app/src/main/res/layout/activity_view_thread.xml
Normal file
38
app/src/main/res/layout/activity_view_thread.xml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/activity_view_thread"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="com.keylesspalace.tusky.ViewThreadActivity">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<android.support.v7.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
android:elevation="4dp"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fragment_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/overlay_fragment_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
7
app/src/main/res/layout/fragment_view_thread.xml
Normal file
7
app/src/main/res/layout/fragment_view_thread.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.v7.widget.RecyclerView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical" />
|
31
app/src/main/res/layout/item_follow.xml
Normal file
31
app/src/main/res/layout/item_follow.xml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="@dimen/status_avatar_column_width"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/notification_side_column">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:srcCompat="@drawable/ic_followed"
|
||||||
|
android:paddingTop="@dimen/notification_icon_vertical_padding"
|
||||||
|
android:paddingBottom="@dimen/notification_icon_vertical_padding"
|
||||||
|
android:paddingRight="@dimen/status_avatar_padding"
|
||||||
|
android:layout_alignParentRight="true" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_toRightOf="@id/notification_side_column"
|
||||||
|
android:id="@+id/notification_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="@dimen/notification_icon_vertical_padding"
|
||||||
|
android:layout_alignParentBottom="true" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:orientation="vertical" android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/notification_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/status_container">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:srcCompat="@drawable/boost_icon"
|
app:srcCompat="@drawable/ic_reblogged"
|
||||||
android:id="@+id/status_boosted_icon"
|
android:id="@+id/status_boosted_icon"
|
||||||
android:adjustViewBounds="false"
|
android:adjustViewBounds="false"
|
||||||
android:cropToPadding="false"
|
android:cropToPadding="false"
|
||||||
|
|
39
app/src/main/res/layout/item_status_notification.xml
Normal file
39
app/src/main/res/layout/item_status_notification.xml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="@dimen/status_avatar_column_width"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/notification_side_column">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/notification_icon"
|
||||||
|
android:paddingTop="@dimen/notification_icon_vertical_padding"
|
||||||
|
android:paddingBottom="@dimen/notification_icon_vertical_padding"
|
||||||
|
android:paddingRight="@dimen/status_avatar_padding"
|
||||||
|
android:layout_alignParentRight="true" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/notification_text"
|
||||||
|
android:layout_toRightOf="@id/notification_side_column"
|
||||||
|
android:paddingBottom="@dimen/notification_icon_vertical_padding" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/notification_content"
|
||||||
|
android:layout_toRightOf="@id/notification_side_column"
|
||||||
|
android:layout_below="@id/notification_text"
|
||||||
|
android:textColor="@color/notification_content_faded"
|
||||||
|
android:paddingBottom="8dp" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
11
app/src/main/res/menu/view_thread_toolbar.xml
Normal file
11
app/src/main/res/menu/view_thread_toolbar.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item android:id="@+id/action_back"
|
||||||
|
android:title="@string/action_back"
|
||||||
|
android:icon="@drawable/ic_back"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
</menu>
|
|
@ -8,4 +8,5 @@
|
||||||
<color name="sensitive_media_warning_background">#303030</color>
|
<color name="sensitive_media_warning_background">#303030</color>
|
||||||
<color name="media_preview_unloaded_background">#DFDFDF</color>
|
<color name="media_preview_unloaded_background">#DFDFDF</color>
|
||||||
<color name="compose_mention">#4F5F6F</color>
|
<color name="compose_mention">#4F5F6F</color>
|
||||||
|
<color name="notification_content_faded">#9F9F9F</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<dimen name="activity_vertical_margin">0dp</dimen>
|
<dimen name="activity_vertical_margin">0dp</dimen>
|
||||||
<dimen name="status_username_left_margin">4dp</dimen>
|
<dimen name="status_username_left_margin">4dp</dimen>
|
||||||
<dimen name="status_since_created_left_margin">4dp</dimen>
|
<dimen name="status_since_created_left_margin">4dp</dimen>
|
||||||
|
<dimen name="status_avatar_column_width">56dp</dimen>
|
||||||
<dimen name="status_avatar_padding">8dp</dimen>
|
<dimen name="status_avatar_padding">8dp</dimen>
|
||||||
<dimen name="status_boost_icon_vertical_padding">5dp</dimen>
|
<dimen name="status_boost_icon_vertical_padding">5dp</dimen>
|
||||||
<dimen name="status_media_preview_top_margin">4dp</dimen>
|
<dimen name="status_media_preview_top_margin">4dp</dimen>
|
||||||
|
@ -12,4 +13,5 @@
|
||||||
<dimen name="compose_media_preview_margin_bottom">16dp</dimen>
|
<dimen name="compose_media_preview_margin_bottom">16dp</dimen>
|
||||||
<dimen name="compose_media_preview_side">48dp</dimen>
|
<dimen name="compose_media_preview_side">48dp</dimen>
|
||||||
<dimen name="compose_mark_sensitive_margin">8dp</dimen>
|
<dimen name="compose_mark_sensitive_margin">8dp</dimen>
|
||||||
|
<dimen name="notification_icon_vertical_padding">4dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
<string name="title_home">Home</string>
|
<string name="title_home">Home</string>
|
||||||
<string name="title_notifications">Notifications</string>
|
<string name="title_notifications">Notifications</string>
|
||||||
<string name="title_public">Public</string>
|
<string name="title_public">Public</string>
|
||||||
|
<string name="title_thread">Thread</string>
|
||||||
|
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s boosted</string>
|
<string name="status_boosted_format">%s boosted</string>
|
||||||
|
@ -60,8 +61,8 @@
|
||||||
|
|
||||||
<string name="footer_text">Could not load the rest of the toots.</string>
|
<string name="footer_text">Could not load the rest of the toots.</string>
|
||||||
|
|
||||||
<string name="notification_reblog_format">%s boosted your status</string>
|
<string name="notification_reblog_format">%s boosted your toot</string>
|
||||||
<string name="notification_favourite_format">%s favourited your status</string>
|
<string name="notification_favourite_format">%s favourited your toot</string>
|
||||||
<string name="notification_follow_format">%s followed you</string>
|
<string name="notification_follow_format">%s followed you</string>
|
||||||
|
|
||||||
<string name="action_compose">Compose</string>
|
<string name="action_compose">Compose</string>
|
||||||
|
@ -74,6 +75,9 @@
|
||||||
<string name="action_retry">Retry</string>
|
<string name="action_retry">Retry</string>
|
||||||
<string name="action_mark_sensitive">Mark Sensitive</string>
|
<string name="action_mark_sensitive">Mark Sensitive</string>
|
||||||
<string name="action_cancel">Cancel</string>
|
<string name="action_cancel">Cancel</string>
|
||||||
|
<string name="action_back">Back</string>
|
||||||
|
|
||||||
|
<string name="confirmation_send">Toot!</string>
|
||||||
|
|
||||||
<string name="description_domain">Domain</string>
|
<string name="description_domain">Domain</string>
|
||||||
<string name="description_compose">What\'s Happening?</string>
|
<string name="description_compose">What\'s Happening?</string>
|
||||||
|
|
Loading…
Reference in a new issue