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:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
<activity android:name=".ViewVideoActivity" />
|
||||
<activity android:name=".ViewThreadActivity" />
|
||||
</application>
|
||||
|
||||
</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() {
|
||||
Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(this, getString(R.string.confirmation_send), Toast.LENGTH_SHORT).show();
|
||||
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) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public boolean hasStatusType() {
|
||||
return type == Type.MENTION
|
||||
|| type == Type.FAVOURITE
|
||||
|| type == Type.REBLOG;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,39 +16,128 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||
private List<Notification> notifications = new ArrayList<>();
|
||||
public class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||
private static final int VIEW_TYPE_MENTION = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
|
||||
private static final int VIEW_TYPE_FOLLOW = 3;
|
||||
|
||||
private List<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
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_notification, parent, false);
|
||||
return new ViewHolder(view);
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_MENTION: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_status, parent, false);
|
||||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new FooterViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_status_notification, parent, false);
|
||||
return new StatusNotificationViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOLLOW: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_follow, parent, false);
|
||||
return new FollowViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
ViewHolder holder = (ViewHolder) viewHolder;
|
||||
Notification notification = notifications.get(position);
|
||||
holder.setMessage(notification.getType(), notification.getDisplayName());
|
||||
if (position < notifications.size()) {
|
||||
Notification notification = notifications.get(position);
|
||||
Notification.Type type = notification.getType();
|
||||
switch (type) {
|
||||
case MENTION: {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
Status status = notification.getStatus();
|
||||
assert(status != null);
|
||||
holder.setupWithStatus(status, statusListener, position);
|
||||
break;
|
||||
}
|
||||
case FAVOURITE:
|
||||
case REBLOG: {
|
||||
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
||||
holder.setMessage(type, notification.getDisplayName(),
|
||||
notification.getStatus());
|
||||
break;
|
||||
}
|
||||
case FOLLOW: {
|
||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
||||
holder.setMessage(notification.getDisplayName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setupButton(footerListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return notifications.size();
|
||||
return notifications.size() + 1;
|
||||
}
|
||||
|
||||
public Notification getItem(int position) {
|
||||
return notifications.get(position);
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == notifications.size()) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
Notification notification = notifications.get(position);
|
||||
switch (notification.getType()) {
|
||||
default:
|
||||
case MENTION: {
|
||||
return VIEW_TYPE_MENTION;
|
||||
}
|
||||
case FAVOURITE:
|
||||
case REBLOG: {
|
||||
return VIEW_TYPE_STATUS_NOTIFICATION;
|
||||
}
|
||||
case FOLLOW: {
|
||||
return VIEW_TYPE_FOLLOW;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Notification getItem(int position) {
|
||||
if (position >= 0 && position < notifications.size()) {
|
||||
return notifications.get(position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int update(List<Notification> new_notifications) {
|
||||
|
@ -76,39 +165,62 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
notifyItemRangeInserted(end, new_notifications.size());
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public void removeItem(int position) {
|
||||
notifications.remove(position);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
public static class FollowViewHolder extends RecyclerView.ViewHolder {
|
||||
private TextView message;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
public FollowViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
message = (TextView) itemView.findViewById(R.id.notification_text);
|
||||
}
|
||||
|
||||
public void setMessage(Notification.Type type, String displayName) {
|
||||
public void setMessage(String displayName) {
|
||||
Context context = message.getContext();
|
||||
String wholeMessage = "";
|
||||
switch (type) {
|
||||
case MENTION: {
|
||||
wholeMessage = displayName + " mentioned you";
|
||||
break;
|
||||
}
|
||||
case REBLOG: {
|
||||
String format = context.getString(R.string.notification_reblog_format);
|
||||
wholeMessage = String.format(format, displayName);
|
||||
break;
|
||||
}
|
||||
case FAVOURITE: {
|
||||
String format = context.getString(R.string.notification_favourite_format);
|
||||
wholeMessage = String.format(format, displayName);
|
||||
break;
|
||||
}
|
||||
case FOLLOW: {
|
||||
String format = context.getString(R.string.notification_follow_format);
|
||||
wholeMessage = String.format(format, displayName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
String format = context.getString(R.string.notification_follow_format);
|
||||
String wholeMessage = String.format(format, displayName);
|
||||
message.setText(wholeMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StatusNotificationViewHolder extends RecyclerView.ViewHolder {
|
||||
private TextView message;
|
||||
private ImageView icon;
|
||||
private TextView statusContent;
|
||||
|
||||
public StatusNotificationViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
message = (TextView) itemView.findViewById(R.id.notification_text);
|
||||
icon = (ImageView) itemView.findViewById(R.id.notification_icon);
|
||||
statusContent = (TextView) itemView.findViewById(R.id.notification_content);
|
||||
}
|
||||
|
||||
public void setMessage(Notification.Type type, String displayName, Status status) {
|
||||
Context context = message.getContext();
|
||||
String format;
|
||||
switch (type) {
|
||||
default:
|
||||
case FAVOURITE: {
|
||||
icon.setImageResource(R.drawable.ic_favourited);
|
||||
format = context.getString(R.string.notification_favourite_format);
|
||||
break;
|
||||
}
|
||||
case REBLOG: {
|
||||
icon.setImageResource(R.drawable.ic_reblogged);
|
||||
format = context.getString(R.string.notification_reblog_format);
|
||||
break;
|
||||
}
|
||||
}
|
||||
String wholeMessage = String.format(format, displayName);
|
||||
message.setText(wholeMessage);
|
||||
String timestamp = DateUtils.getRelativeTimeSpanString(
|
||||
status.getCreatedAt().getTime(),
|
||||
new Date().getTime());
|
||||
statusContent.setText(String.format("%s: ", timestamp));
|
||||
statusContent.append(status.getContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,12 +172,10 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
sendFetchNotificationsRequest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadMore() {
|
||||
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (notification != null) {
|
||||
|
@ -187,32 +185,32 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReply(int position) {
|
||||
Notification notification = adapter.getItem(position);
|
||||
super.reply(notification.getStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReblog(boolean reblog, int position) {
|
||||
Notification notification = adapter.getItem(position);
|
||||
super.reblog(notification.getStatus(), reblog, adapter, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFavourite(boolean favourite, int position) {
|
||||
Notification notification = adapter.getItem(position);
|
||||
super.favourite(notification.getStatus(), favourite, adapter, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMore(View view, int position) {
|
||||
Notification notification = adapter.getItem(position);
|
||||
super.more(notification.getStatus(), view, adapter, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewMedia(String url, Status.MediaAttachment.Type type) {
|
||||
super.viewMedia(url, type);
|
||||
}
|
||||
|
||||
public void onViewThread(int position) {
|
||||
Notification notification = adapter.getItem(position);
|
||||
super.viewThread(notification.getStatus());
|
||||
}
|
||||
}
|
||||
|
|
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 onMore(View view, final int position);
|
||||
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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.PagerSnapHelper;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.volley.toolbox.ImageLoader;
|
||||
import com.android.volley.toolbox.NetworkImageView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class TimelineAdapter extends RecyclerView.Adapter {
|
||||
public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||
private static final int VIEW_TYPE_STATUS = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
|
@ -76,32 +62,7 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
if (position < statuses.size()) {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
Status status = statuses.get(position);
|
||||
holder.setDisplayName(status.getDisplayName());
|
||||
holder.setUsername(status.getUsername());
|
||||
holder.setCreatedAt(status.getCreatedAt());
|
||||
holder.setContent(status.getContent());
|
||||
holder.setAvatar(status.getAvatar());
|
||||
holder.setContent(status.getContent());
|
||||
holder.setReblogged(status.getReblogged());
|
||||
holder.setFavourited(status.getFavourited());
|
||||
String rebloggedByUsername = status.getRebloggedByUsername();
|
||||
if (rebloggedByUsername == null) {
|
||||
holder.hideRebloggedByUsername();
|
||||
} else {
|
||||
holder.setRebloggedByUsername(rebloggedByUsername);
|
||||
}
|
||||
Status.MediaAttachment[] attachments = status.getAttachments();
|
||||
boolean sensitive = status.getSensitive();
|
||||
holder.setMediaPreviews(attachments, sensitive, statusListener);
|
||||
/* A status without attachments is sometimes still marked sensitive, so it's necessary
|
||||
* to check both whether there are any attachments and if it's marked sensitive. */
|
||||
if (!sensitive || attachments.length == 0) {
|
||||
holder.hideSensitiveMediaWarning();
|
||||
}
|
||||
holder.setupButtons(statusListener, position);
|
||||
if (status.getVisibility() == Status.Visibility.PRIVATE) {
|
||||
holder.disableReblogging();
|
||||
}
|
||||
holder.setupWithStatus(status, statusListener, position);
|
||||
} else {
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.setupButton(footerListener);
|
||||
|
@ -158,267 +119,4 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class StatusViewHolder extends RecyclerView.ViewHolder {
|
||||
private TextView displayName;
|
||||
private TextView username;
|
||||
private TextView sinceCreated;
|
||||
private TextView content;
|
||||
private NetworkImageView avatar;
|
||||
private ImageView boostedIcon;
|
||||
private TextView boostedByUsername;
|
||||
private ImageButton replyButton;
|
||||
private ImageButton reblogButton;
|
||||
private ImageButton favouriteButton;
|
||||
private ImageButton moreButton;
|
||||
private boolean favourited;
|
||||
private boolean reblogged;
|
||||
private NetworkImageView mediaPreview0;
|
||||
private NetworkImageView mediaPreview1;
|
||||
private NetworkImageView mediaPreview2;
|
||||
private NetworkImageView mediaPreview3;
|
||||
private View sensitiveMediaWarning;
|
||||
|
||||
public StatusViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
displayName = (TextView) itemView.findViewById(R.id.status_display_name);
|
||||
username = (TextView) itemView.findViewById(R.id.status_username);
|
||||
sinceCreated = (TextView) itemView.findViewById(R.id.status_since_created);
|
||||
content = (TextView) itemView.findViewById(R.id.status_content);
|
||||
avatar = (NetworkImageView) itemView.findViewById(R.id.status_avatar);
|
||||
boostedIcon = (ImageView) itemView.findViewById(R.id.status_boosted_icon);
|
||||
boostedByUsername = (TextView) itemView.findViewById(R.id.status_boosted);
|
||||
replyButton = (ImageButton) itemView.findViewById(R.id.status_reply);
|
||||
reblogButton = (ImageButton) itemView.findViewById(R.id.status_reblog);
|
||||
favouriteButton = (ImageButton) itemView.findViewById(R.id.status_favourite);
|
||||
moreButton = (ImageButton) itemView.findViewById(R.id.status_more);
|
||||
reblogged = false;
|
||||
favourited = false;
|
||||
mediaPreview0 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_0);
|
||||
mediaPreview1 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_1);
|
||||
mediaPreview2 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_2);
|
||||
mediaPreview3 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_3);
|
||||
mediaPreview0.setDefaultImageResId(R.drawable.media_preview_unloaded);
|
||||
mediaPreview1.setDefaultImageResId(R.drawable.media_preview_unloaded);
|
||||
mediaPreview2.setDefaultImageResId(R.drawable.media_preview_unloaded);
|
||||
mediaPreview3.setDefaultImageResId(R.drawable.media_preview_unloaded);
|
||||
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning);
|
||||
}
|
||||
|
||||
public void setDisplayName(String name) {
|
||||
displayName.setText(name);
|
||||
}
|
||||
|
||||
public void setUsername(String name) {
|
||||
Context context = username.getContext();
|
||||
String format = context.getString(R.string.status_username_format);
|
||||
String usernameText = String.format(format, name);
|
||||
username.setText(usernameText);
|
||||
}
|
||||
|
||||
public void setContent(Spanned content) {
|
||||
this.content.setText(content);
|
||||
}
|
||||
|
||||
public void setAvatar(String url) {
|
||||
Context context = avatar.getContext();
|
||||
ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader();
|
||||
avatar.setImageUrl(url, imageLoader);
|
||||
avatar.setDefaultImageResId(R.drawable.avatar_default);
|
||||
avatar.setErrorImageResId(R.drawable.avatar_error);
|
||||
}
|
||||
|
||||
/* This is a rough duplicate of android.text.format.DateUtils.getRelativeTimeSpanString,
|
||||
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough. */
|
||||
private String getRelativeTimeSpanString(long then, long now) {
|
||||
final long MINUTE = 60;
|
||||
final long HOUR = 60 * MINUTE;
|
||||
final long DAY = 24 * HOUR;
|
||||
final long YEAR = 365 * DAY;
|
||||
long span = (now - then) / 1000;
|
||||
String prefix = "";
|
||||
if (span < 0) {
|
||||
prefix = "in ";
|
||||
span = -span;
|
||||
}
|
||||
String unit;
|
||||
if (span < MINUTE) {
|
||||
unit = "s";
|
||||
} else if (span < HOUR) {
|
||||
span /= MINUTE;
|
||||
unit = "m";
|
||||
} else if (span < DAY) {
|
||||
span /= HOUR;
|
||||
unit = "h";
|
||||
} else if (span < YEAR) {
|
||||
span /= DAY;
|
||||
unit = "d";
|
||||
} else {
|
||||
span /= YEAR;
|
||||
unit = "y";
|
||||
}
|
||||
return prefix + span + unit;
|
||||
}
|
||||
|
||||
public void setCreatedAt(@Nullable Date createdAt) {
|
||||
String readout;
|
||||
if (createdAt != null) {
|
||||
long then = createdAt.getTime();
|
||||
long now = new Date().getTime();
|
||||
readout = getRelativeTimeSpanString(then, now);
|
||||
} else {
|
||||
readout = "?m"; // unknown minutes~
|
||||
}
|
||||
sinceCreated.setText(readout);
|
||||
}
|
||||
|
||||
public void setRebloggedByUsername(String name) {
|
||||
Context context = boostedByUsername.getContext();
|
||||
String format = context.getString(R.string.status_boosted_format);
|
||||
String boostedText = String.format(format, name);
|
||||
boostedByUsername.setText(boostedText);
|
||||
boostedIcon.setVisibility(View.VISIBLE);
|
||||
boostedByUsername.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void hideRebloggedByUsername() {
|
||||
boostedIcon.setVisibility(View.GONE);
|
||||
boostedByUsername.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setReblogged(boolean reblogged) {
|
||||
this.reblogged = reblogged;
|
||||
if (!reblogged) {
|
||||
reblogButton.setImageResource(R.drawable.ic_reblog_off);
|
||||
} else {
|
||||
reblogButton.setImageResource(R.drawable.ic_reblog_on);
|
||||
}
|
||||
}
|
||||
|
||||
public void disableReblogging() {
|
||||
reblogButton.setEnabled(false);
|
||||
reblogButton.setImageResource(R.drawable.ic_reblog_disabled);
|
||||
}
|
||||
|
||||
public void setFavourited(boolean favourited) {
|
||||
this.favourited = favourited;
|
||||
if (!favourited) {
|
||||
favouriteButton.setImageResource(R.drawable.ic_favourite_off);
|
||||
} else {
|
||||
favouriteButton.setImageResource(R.drawable.ic_favourite_on);
|
||||
}
|
||||
}
|
||||
|
||||
public void setMediaPreviews(final Status.MediaAttachment[] attachments,
|
||||
boolean sensitive, final StatusActionListener listener) {
|
||||
final NetworkImageView[] previews = {
|
||||
mediaPreview0,
|
||||
mediaPreview1,
|
||||
mediaPreview2,
|
||||
mediaPreview3
|
||||
};
|
||||
Context context = mediaPreview0.getContext();
|
||||
ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader();
|
||||
final int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS);
|
||||
for (int i = 0; i < n; i++) {
|
||||
String previewUrl = attachments[i].getPreviewUrl();
|
||||
previews[i].setImageUrl(previewUrl, imageLoader);
|
||||
if (!sensitive) {
|
||||
previews[i].setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
previews[i].setVisibility(View.GONE);
|
||||
}
|
||||
final String url = attachments[i].getUrl();
|
||||
final Status.MediaAttachment.Type type = attachments[i].getType();
|
||||
previews[i].setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onViewMedia(url, type);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (sensitive) {
|
||||
sensitiveMediaWarning.setVisibility(View.VISIBLE);
|
||||
sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
v.setVisibility(View.GONE);
|
||||
for (int i = 0; i < n; i++) {
|
||||
previews[i].setVisibility(View.VISIBLE);
|
||||
}
|
||||
v.setOnClickListener(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Hide any of the placeholder previews beyond the ones set.
|
||||
for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) {
|
||||
previews[i].setImageUrl(null, imageLoader);
|
||||
previews[i].setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void hideSensitiveMediaWarning() {
|
||||
sensitiveMediaWarning.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setupButtons(final StatusActionListener listener, final int position) {
|
||||
replyButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onReply(position);
|
||||
}
|
||||
});
|
||||
reblogButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onReblog(!reblogged, position);
|
||||
}
|
||||
});
|
||||
favouriteButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onFavourite(!favourited, position);
|
||||
}
|
||||
});
|
||||
moreButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onMore(v, position);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class FooterViewHolder extends RecyclerView.ViewHolder {
|
||||
private LinearLayout retryBar;
|
||||
private Button retry;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
public FooterViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
retryBar = (LinearLayout) itemView.findViewById(R.id.footer_retry_bar);
|
||||
retry = (Button) itemView.findViewById(R.id.footer_retry_button);
|
||||
progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar);
|
||||
progressBar.setIndeterminate(true);
|
||||
}
|
||||
|
||||
public void setupButton(final FooterActionListener listener) {
|
||||
retry.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onLoadMore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void showRetry(boolean show) {
|
||||
if (!show) {
|
||||
retryBar.setVisibility(View.GONE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
retryBar.setVisibility(View.VISIBLE);
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,42 +16,31 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.PopupMenu;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.JsonArrayRequest;
|
||||
import com.android.volley.toolbox.JsonObjectRequest;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TimelineFragment extends Fragment implements
|
||||
public class TimelineFragment extends SFragment implements
|
||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener {
|
||||
|
||||
public enum Kind {
|
||||
|
@ -60,12 +49,6 @@ public class TimelineFragment extends Fragment implements
|
|||
PUBLIC,
|
||||
}
|
||||
|
||||
private String domain = null;
|
||||
private String accessToken = null;
|
||||
/** ID of the account that is currently logged-in. */
|
||||
private String userAccountId = null;
|
||||
/** Username of the account that is currently logged-in. */
|
||||
private String userUsername = null;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private RecyclerView recyclerView;
|
||||
private TimelineAdapter adapter;
|
||||
|
@ -90,15 +73,8 @@ public class TimelineFragment extends Fragment implements
|
|||
|
||||
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
||||
|
||||
Context context = getContext();
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
domain = preferences.getString("domain", null);
|
||||
accessToken = preferences.getString("accessToken", null);
|
||||
assert(domain != null);
|
||||
assert(accessToken != null);
|
||||
|
||||
// Setup the SwipeRefreshLayout.
|
||||
Context context = getContext();
|
||||
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
|
||||
swipeRefreshLayout.setOnRefreshListener(this);
|
||||
// Setup the RecyclerView.
|
||||
|
@ -121,7 +97,6 @@ public class TimelineFragment extends Fragment implements
|
|||
} else {
|
||||
sendFetchTimelineRequest();
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
|
@ -143,7 +118,6 @@ public class TimelineFragment extends Fragment implements
|
|||
};
|
||||
layout.addOnTabSelectedListener(onTabSelectedListener);
|
||||
|
||||
sendUserInfoRequest();
|
||||
sendFetchTimelineRequest();
|
||||
|
||||
return rootView;
|
||||
|
@ -161,22 +135,6 @@ public class TimelineFragment extends Fragment implements
|
|||
scrollListener.reset();
|
||||
}
|
||||
|
||||
private void sendUserInfoRequest() {
|
||||
sendRequest(Request.Method.GET, getString(R.string.endpoint_verify_credentials), null,
|
||||
new Response.Listener<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) {
|
||||
String endpoint;
|
||||
switch (kind) {
|
||||
|
@ -251,7 +209,7 @@ public class TimelineFragment extends Fragment implements
|
|||
RecyclerView.ViewHolder viewHolder =
|
||||
recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1);
|
||||
if (viewHolder != null) {
|
||||
TimelineAdapter.FooterViewHolder holder = (TimelineAdapter.FooterViewHolder) viewHolder;
|
||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
||||
holder.showRetry(show);
|
||||
}
|
||||
}
|
||||
|
@ -260,163 +218,6 @@ public class TimelineFragment extends Fragment implements
|
|||
sendFetchTimelineRequest();
|
||||
}
|
||||
|
||||
private void sendRequest(
|
||||
int method, String endpoint, JSONObject parameters,
|
||||
@Nullable Response.Listener<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() {
|
||||
Status status = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (status != null) {
|
||||
|
@ -425,4 +226,28 @@ public class TimelineFragment extends Fragment implements
|
|||
sendFetchTimelineRequest();
|
||||
}
|
||||
}
|
||||
|
||||
public void onReply(int position) {
|
||||
super.reply(adapter.getItem(position));
|
||||
}
|
||||
|
||||
public void onReblog(final boolean reblog, final int position) {
|
||||
super.reblog(adapter.getItem(position), reblog, adapter, position);
|
||||
}
|
||||
|
||||
public void onFavourite(final boolean favourite, final int position) {
|
||||
super.favourite(adapter.getItem(position), favourite, adapter, position);
|
||||
}
|
||||
|
||||
public void onMore(View view, final int position) {
|
||||
super.more(adapter.getItem(position), view, adapter, position);
|
||||
}
|
||||
|
||||
public void onViewMedia(String url, Status.MediaAttachment.Type type) {
|
||||
super.viewMedia(url, type);
|
||||
}
|
||||
|
||||
public void onViewThread(int position) {
|
||||
super.viewThread(adapter.getItem(position));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/status_container">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -17,7 +17,7 @@
|
|||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/boost_icon"
|
||||
app:srcCompat="@drawable/ic_reblogged"
|
||||
android:id="@+id/status_boosted_icon"
|
||||
android:adjustViewBounds="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="media_preview_unloaded_background">#DFDFDF</color>
|
||||
<color name="compose_mention">#4F5F6F</color>
|
||||
<color name="notification_content_faded">#9F9F9F</color>
|
||||
</resources>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<dimen name="activity_vertical_margin">0dp</dimen>
|
||||
<dimen name="status_username_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_boost_icon_vertical_padding">5dp</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_side">48dp</dimen>
|
||||
<dimen name="compose_mark_sensitive_margin">8dp</dimen>
|
||||
<dimen name="notification_icon_vertical_padding">4dp</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
<string name="title_home">Home</string>
|
||||
<string name="title_notifications">Notifications</string>
|
||||
<string name="title_public">Public</string>
|
||||
<string name="title_thread">Thread</string>
|
||||
|
||||
<string name="status_username_format">\@%s</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="notification_reblog_format">%s boosted your status</string>
|
||||
<string name="notification_favourite_format">%s favourited your status</string>
|
||||
<string name="notification_reblog_format">%s boosted your toot</string>
|
||||
<string name="notification_favourite_format">%s favourited your toot</string>
|
||||
<string name="notification_follow_format">%s followed you</string>
|
||||
|
||||
<string name="action_compose">Compose</string>
|
||||
|
@ -74,6 +75,9 @@
|
|||
<string name="action_retry">Retry</string>
|
||||
<string name="action_mark_sensitive">Mark Sensitive</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_compose">What\'s Happening?</string>
|
||||
|
|
Loading…
Reference in a new issue