Jumping to top capability and a progress/retry footer added to timelines.

This commit is contained in:
Vavassor 2017-01-18 13:35:07 -05:00
parent 6b684bceff
commit 2106d7a53c
9 changed files with 300 additions and 74 deletions

View File

@ -4,6 +4,7 @@ import android.Manifest;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
@ -55,6 +56,7 @@ import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
@ -196,21 +198,11 @@ public class ComposeActivity extends AppCompatActivity {
VolleySingleton.getInstance(this).addToRequestQueue(request); VolleySingleton.getInstance(this).addToRequestQueue(request);
} }
private void onReadyFailure(Exception exception, final String content,
final String visibility, final boolean sensitive) {
doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry,
new View.OnClickListener() {
@Override
public void onClick(View v) {
readyStatus(content, visibility, sensitive);
}
});
}
private void readyStatus(final String content, final String visibility, private void readyStatus(final String content, final String visibility,
final boolean sensitive) { final boolean sensitive) {
final ProgressDialog dialog = ProgressDialog.show(this, "Finishing Media Upload", final ProgressDialog dialog = ProgressDialog.show(this, "Finishing Media Upload",
"Uploading...", true); "Uploading...", true, true);
final AsyncTask<Void, Void, Boolean> waitForMediaTask =
new AsyncTask<Void, Void, Boolean>() { new AsyncTask<Void, Void, Boolean>() {
private Exception exception; private Exception exception;
@ -235,7 +227,34 @@ public class ComposeActivity extends AppCompatActivity {
onReadyFailure(exception, content, visibility, sensitive); onReadyFailure(exception, content, visibility, sensitive);
} }
} }
}.execute();
@Override
protected void onCancelled() {
removeAllMediaFromQueue();
super.onCancelled();
}
};
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
/* Generating an interrupt by passing true here is important because an interrupt
* exception is the only thing that will kick the latch out of its waiting loop
* early. */
waitForMediaTask.cancel(true);
}
});
waitForMediaTask.execute();
}
private void onReadyFailure(Exception exception, final String content, final String visibility,
final boolean sensitive) {
doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry,
new View.OnClickListener() {
@Override
public void onClick(View v) {
readyStatus(content, visibility, sensitive);
}
});
} }
@Override @Override
@ -439,16 +458,26 @@ public class ComposeActivity extends AppCompatActivity {
cancelReadyingMedia(item); cancelReadyingMedia(item);
} }
private void removeAllMediaFromQueue() {
for (Iterator<QueuedMedia> it = mediaQueued.iterator(); it.hasNext();) {
QueuedMedia item = it.next();
it.remove();
removeMediaFromQueue(item);
}
}
private void downsizeMedia(final QueuedMedia item) { private void downsizeMedia(final QueuedMedia item) {
item.setReadyStage(QueuedMedia.ReadyStage.DOWNSIZING); item.setReadyStage(QueuedMedia.ReadyStage.DOWNSIZING);
InputStream stream; InputStream stream = null;
try { try {
stream = getContentResolver().openInputStream(item.getUri()); stream = getContentResolver().openInputStream(item.getUri());
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
IOUtils.closeQuietly(stream);
onMediaDownsizeFailure(item); onMediaDownsizeFailure(item);
return; return;
} }
Bitmap bitmap = BitmapFactory.decodeStream(stream); Bitmap bitmap = BitmapFactory.decodeStream(stream);
IOUtils.closeQuietly(stream);
new DownsizeImageTask(STATUS_MEDIA_SIZE_LIMIT, new DownsizeImageTask.Listener() { new DownsizeImageTask(STATUS_MEDIA_SIZE_LIMIT, new DownsizeImageTask.Listener() {
@Override @Override
public void onSuccess(List<byte[]> contentList) { public void onSuccess(List<byte[]> contentList) {
@ -538,13 +567,15 @@ public class ComposeActivity extends AppCompatActivity {
public DataItem getData() { public DataItem getData() {
byte[] content = item.getContent(); byte[] content = item.getContent();
if (content == null) { if (content == null) {
InputStream stream; InputStream stream = null;
try { try {
stream = getContentResolver().openInputStream(item.getUri()); stream = getContentResolver().openInputStream(item.getUri());
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
IOUtils.closeQuietly(stream);
return null; return null;
} }
content = inputStreamGetBytes(stream); content = inputStreamGetBytes(stream);
IOUtils.closeQuietly(stream);
if (content == null) { if (content == null) {
return null; return null;
} }
@ -607,10 +638,11 @@ public class ComposeActivity extends AppCompatActivity {
break; break;
} }
case "image": { case "image": {
InputStream stream; InputStream stream = null;
try { try {
stream = contentResolver.openInputStream(uri); stream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
IOUtils.closeQuietly(stream);
displayTransientError(R.string.error_media_upload_opening); displayTransientError(R.string.error_media_upload_opening);
return; return;
} }
@ -618,7 +650,9 @@ public class ComposeActivity extends AppCompatActivity {
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 96, 96); Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 96, 96);
source.recycle(); source.recycle();
try { try {
if (stream != null) {
stream.close(); stream.close();
}
} catch (IOException e) { } catch (IOException e) {
bitmap.recycle(); bitmap.recycle();
displayTransientError(R.string.error_media_upload_opening); displayTransientError(R.string.error_media_upload_opening);

View File

@ -0,0 +1,5 @@
package com.keylesspalace.tusky;
public interface FooterActionListener {
void onLoadMore();
}

View File

@ -0,0 +1,18 @@
package com.keylesspalace.tusky;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
public class IOUtils {
public static void closeQuietly(@Nullable InputStream stream) {
try {
if (stream != null) {
stream.close();
}
} catch (IOException e) {
// intentionally unhandled
}
}
}

View File

@ -9,8 +9,11 @@ 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.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.ImageLoader;
@ -21,25 +24,42 @@ import java.util.Date;
import java.util.List; import java.util.List;
public class TimelineAdapter extends RecyclerView.Adapter { public class TimelineAdapter extends RecyclerView.Adapter {
private List<Status> statuses = new ArrayList<>(); private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_FOOTER = 1;
StatusActionListener listener; private List<Status> statuses;
private StatusActionListener statusListener;
private FooterActionListener footerListener;
public TimelineAdapter(StatusActionListener listener) { public TimelineAdapter(StatusActionListener statusListener,
FooterActionListener footerListener) {
super(); super();
this.listener = listener; statuses = new ArrayList<>();
this.statusListener = statusListener;
this.footerListener = footerListener;
} }
@Override @Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
View v = LayoutInflater.from(viewGroup.getContext()) switch (viewType) {
default:
case VIEW_TYPE_STATUS: {
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_status, viewGroup, false); .inflate(R.layout.item_status, viewGroup, false);
return new ViewHolder(v); return new StatusViewHolder(view);
}
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer, viewGroup, false);
return new FooterViewHolder(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 < statuses.size()) {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
Status status = statuses.get(position); Status status = statuses.get(position);
holder.setDisplayName(status.getDisplayName()); holder.setDisplayName(status.getDisplayName());
holder.setUsername(status.getUsername()); holder.setUsername(status.getUsername());
@ -57,35 +77,48 @@ public class TimelineAdapter extends RecyclerView.Adapter {
} }
Status.MediaAttachment[] attachments = status.getAttachments(); Status.MediaAttachment[] attachments = status.getAttachments();
boolean sensitive = status.getSensitive(); boolean sensitive = status.getSensitive();
holder.setMediaPreviews(attachments, sensitive, listener); holder.setMediaPreviews(attachments, sensitive, statusListener);
/* A status without attachments is sometimes still marked sensitive, so it's necessary /* 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. */ * to check both whether there are any attachments and if it's marked sensitive. */
if (!sensitive || attachments.length == 0) { if (!sensitive || attachments.length == 0) {
holder.hideSensitiveMediaWarning(); holder.hideSensitiveMediaWarning();
} }
holder.setupButtons(listener, position); holder.setupButtons(statusListener, position);
if (status.getVisibility() == Status.Visibility.PRIVATE) { if (status.getVisibility() == Status.Visibility.PRIVATE) {
holder.disableReblogging(); holder.disableReblogging();
} }
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setupButton(footerListener);
}
} }
@Override @Override
public int getItemCount() { public int getItemCount() {
return statuses.size(); return statuses.size() + 1;
} }
public int update(List<Status> new_statuses) { @Override
public int getItemViewType(int position) {
if (position == statuses.size()) {
return VIEW_TYPE_FOOTER;
} else {
return VIEW_TYPE_STATUS;
}
}
public int update(List<Status> newStatuses) {
int scrollToPosition; int scrollToPosition;
if (statuses == null || statuses.isEmpty()) { if (statuses == null || statuses.isEmpty()) {
statuses = new_statuses; statuses = newStatuses;
scrollToPosition = 0; scrollToPosition = 0;
} else { } else {
int index = new_statuses.indexOf(statuses.get(0)); int index = newStatuses.indexOf(statuses.get(0));
if (index == -1) { if (index == -1) {
statuses.addAll(0, new_statuses); statuses.addAll(0, newStatuses);
scrollToPosition = 0; scrollToPosition = 0;
} else { } else {
statuses.addAll(0, new_statuses.subList(0, index)); statuses.addAll(0, newStatuses.subList(0, index));
scrollToPosition = index; scrollToPosition = index;
} }
} }
@ -93,10 +126,10 @@ public class TimelineAdapter extends RecyclerView.Adapter {
return scrollToPosition; return scrollToPosition;
} }
public void addItems(List<Status> new_statuses) { public void addItems(List<Status> newStatuses) {
int end = statuses.size(); int end = statuses.size();
statuses.addAll(new_statuses); statuses.addAll(newStatuses);
notifyItemRangeInserted(end, new_statuses.size()); notifyItemRangeInserted(end, newStatuses.size());
} }
public void removeItem(int position) { public void removeItem(int position) {
@ -104,11 +137,14 @@ public class TimelineAdapter extends RecyclerView.Adapter {
notifyItemRemoved(position); notifyItemRemoved(position);
} }
public Status getItem(int position) { public @Nullable Status getItem(int position) {
if (position >= 0 && position < statuses.size()) {
return statuses.get(position); return statuses.get(position);
} }
return null;
}
public static class ViewHolder extends RecyclerView.ViewHolder { public static class StatusViewHolder extends RecyclerView.ViewHolder {
private TextView displayName; private TextView displayName;
private TextView username; private TextView username;
private TextView sinceCreated; private TextView sinceCreated;
@ -128,7 +164,7 @@ public class TimelineAdapter extends RecyclerView.Adapter {
private NetworkImageView mediaPreview3; private NetworkImageView mediaPreview3;
private View sensitiveMediaWarning; private View sensitiveMediaWarning;
public ViewHolder(View itemView) { public StatusViewHolder(View itemView) {
super(itemView); super(itemView);
displayName = (TextView) itemView.findViewById(R.id.status_display_name); displayName = (TextView) itemView.findViewById(R.id.status_display_name);
username = (TextView) itemView.findViewById(R.id.status_username); username = (TextView) itemView.findViewById(R.id.status_username);
@ -331,4 +367,37 @@ public class TimelineAdapter extends RecyclerView.Adapter {
}); });
} }
} }
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);
}
}
}
} }

View File

@ -6,6 +6,7 @@ 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.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
@ -18,7 +19,6 @@ import android.view.LayoutInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import com.android.volley.AuthFailureError; import com.android.volley.AuthFailureError;
import com.android.volley.Request; import com.android.volley.Request;
@ -36,7 +36,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public class TimelineFragment extends Fragment implements public class TimelineFragment extends Fragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener { SwipeRefreshLayout.OnRefreshListener, StatusActionListener, FooterActionListener {
public enum Kind { public enum Kind {
HOME, HOME,
@ -48,8 +48,12 @@ public class TimelineFragment extends Fragment implements
private String accessToken = null; private String accessToken = null;
private String userAccountId = null; private String userAccountId = null;
private SwipeRefreshLayout swipeRefreshLayout; private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
private TimelineAdapter adapter; private TimelineAdapter adapter;
private Kind kind; private Kind kind;
private LinearLayoutManager layoutManager;
private EndlessOnScrollListener scrollListener;
private TabLayout.OnTabSelectedListener onTabSelectedListener;
public static TimelineFragment newInstance(Kind kind) { public static TimelineFragment newInstance(Kind kind) {
TimelineFragment fragment = new TimelineFragment(); TimelineFragment fragment = new TimelineFragment();
@ -79,33 +83,65 @@ public class TimelineFragment extends Fragment implements
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.
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view); recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true); recyclerView.setHasFixedSize(true);
LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager); recyclerView.setLayoutManager(layoutManager);
DividerItemDecoration divider = new DividerItemDecoration( DividerItemDecoration divider = new DividerItemDecoration(
context, layoutManager.getOrientation()); context, layoutManager.getOrientation());
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.status_divider); Drawable drawable = ContextCompat.getDrawable(context, R.drawable.status_divider);
divider.setDrawable(drawable); divider.setDrawable(drawable);
recyclerView.addItemDecoration(divider); recyclerView.addItemDecoration(divider);
EndlessOnScrollListener scrollListener = new EndlessOnScrollListener(layoutManager) { scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override @Override
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) { public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
TimelineAdapter adapter = (TimelineAdapter) view.getAdapter(); TimelineAdapter adapter = (TimelineAdapter) view.getAdapter();
String fromId = adapter.getItem(adapter.getItemCount() - 1).getId(); Status status = adapter.getItem(adapter.getItemCount() - 2);
sendFetchTimelineRequest(fromId); if (status != null) {
sendFetchTimelineRequest(status.getId());
} else {
sendFetchTimelineRequest();
}
} }
}; };
recyclerView.addOnScrollListener(scrollListener); recyclerView.addOnScrollListener(scrollListener);
adapter = new TimelineAdapter(this); adapter = new TimelineAdapter(this, this);
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
TabLayout layout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
jumpToTop();
}
};
layout.addOnTabSelectedListener(onTabSelectedListener);
sendUserInfoRequest(); sendUserInfoRequest();
sendFetchTimelineRequest(); sendFetchTimelineRequest();
return rootView; return rootView;
} }
@Override
public void onDestroyView() {
TabLayout tabLayout = (TabLayout) getActivity().findViewById(R.id.tab_layout);
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
super.onDestroyView();
}
private void jumpToTop() {
layoutManager.scrollToPositionWithOffset(0, 0);
scrollListener.reset();
}
private void sendUserInfoRequest() { private void sendUserInfoRequest() {
sendRequest(Request.Method.GET, getString(R.string.endpoint_verify_credentials), null, sendRequest(Request.Method.GET, getString(R.string.endpoint_verify_credentials), null,
new Response.Listener<JSONObject>() { new Response.Listener<JSONObject>() {
@ -182,15 +218,24 @@ public class TimelineFragment extends Fragment implements
} else { } else {
adapter.update(statuses); adapter.update(statuses);
} }
showFetchTimelineRetry(false);
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
} }
public void onFetchTimelineFailure(Exception exception) { public void onFetchTimelineFailure(Exception exception) {
Toast.makeText(getContext(), R.string.error_fetching_timeline, Toast.LENGTH_SHORT) showFetchTimelineRetry(true);
.show();
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
} }
private void showFetchTimelineRetry(boolean show) {
RecyclerView.ViewHolder viewHolder =
recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1);
if (viewHolder != null) {
TimelineAdapter.FooterViewHolder holder = (TimelineAdapter.FooterViewHolder) viewHolder;
holder.showRetry(show);
}
}
public void onRefresh() { public void onRefresh() {
sendFetchTimelineRequest(); sendFetchTimelineRequest();
} }
@ -335,4 +380,13 @@ public class TimelineFragment extends Fragment implements
} }
} }
} }
public void onLoadMore() {
Status status = adapter.getItem(adapter.getItemCount() - 2);
if (status != null) {
sendFetchTimelineRequest(status.getId());
} else {
sendFetchTimelineRequest();
}
}
} }

View File

@ -49,6 +49,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/title_public" /> android:text="@string/title_public" />
</android.support.design.widget.TabLayout> </android.support.design.widget.TabLayout>
</android.support.v4.view.ViewPager> </android.support.v4.view.ViewPager>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center">
<LinearLayout
android:id="@+id/footer_retry_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/footer_text"
android:padding="@dimen/footer_text_padding"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/footer_retry_button"
android:text="@string/action_retry" />
</LinearLayout>
<ProgressBar
android:id="@+id/footer_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>

View File

@ -7,6 +7,7 @@
<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>
<dimen name="status_media_preview_height">96dp</dimen> <dimen name="status_media_preview_height">96dp</dimen>
<dimen name="footer_text_padding">8dp</dimen>
<dimen name="compose_media_preview_margin">8dp</dimen> <dimen name="compose_media_preview_margin">8dp</dimen>
<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>

View File

@ -58,6 +58,8 @@
<string name="status_sensitive_media_title">Sensitive Media</string> <string name="status_sensitive_media_title">Sensitive Media</string>
<string name="status_sensitive_media_directions">Click to view.</string> <string name="status_sensitive_media_directions">Click to view.</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 status</string>
<string name="notification_favourite_format">%s favourited your status</string> <string name="notification_favourite_format">%s favourited your status</string>
<string name="notification_follow_format">%s followed you</string> <string name="notification_follow_format">%s followed you</string>
@ -71,6 +73,7 @@
<string name="action_send">TOOT</string> <string name="action_send">TOOT</string>
<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="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>