Merge branch 'master' into edit-profile

This commit is contained in:
Vavassor 2017-04-17 01:10:20 -04:00
commit e15f1cfcab
21 changed files with 648 additions and 89 deletions

View file

@ -101,6 +101,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
private static final int MEDIA_PICK_RESULT = 1;
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
private static final int MEDIA_SIZE_UNKNOWN = -1;
private static final int COMPOSE_SUCCESS = -1;
private String inReplyToId;
private EditText textEditor;
@ -122,6 +123,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
private ImageButton pickBtn;
private Button nsfwBtn;
private ProgressBar postProgress;
private ImageButton visibilityBtn;
private static class QueuedMedia {
enum Type {
@ -341,17 +343,11 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
floatingBtn = (Button) findViewById(R.id.floating_btn);
pickBtn = (ImageButton) findViewById(R.id.compose_photo_pick);
nsfwBtn = (Button) findViewById(R.id.action_toggle_nsfw);
final ImageButton visibilityBtn = (ImageButton) findViewById(R.id.action_toggle_visibility);
visibilityBtn = (ImageButton) findViewById(R.id.action_toggle_visibility);
floatingBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
pickBtn.setClickable(false);
nsfwBtn.setClickable(false);
visibilityBtn.setClickable(false);
floatingBtn.setEnabled(false);
postProgress.setVisibility(View.VISIBLE);
sendStatus();
}
});
@ -567,6 +563,20 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
}
}
private void disableButtons() {
pickBtn.setClickable(false);
nsfwBtn.setClickable(false);
visibilityBtn.setClickable(false);
floatingBtn.setEnabled(false);
}
private void enableButtons() {
pickBtn.setClickable(true);
nsfwBtn.setClickable(true);
visibilityBtn.setClickable(true);
floatingBtn.setEnabled(true);
}
private void setStatusVisibility(String visibility) {
statusVisibility = visibility;
switch (visibility) {
@ -615,6 +625,18 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
updateVisibleCharactersLeft();
}
void setStateToReadying() {
statusAlreadyInFlight = true;
disableButtons();
postProgress.setVisibility(View.VISIBLE);
}
void setStateToNotReadying() {
postProgress.setVisibility(View.INVISIBLE);
statusAlreadyInFlight = false;
enableButtons();
}
private void sendStatus() {
if (statusAlreadyInFlight) {
return;
@ -624,9 +646,12 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
if (statusHideText) {
spoilerText = contentWarningEditor.getText().toString();
}
if (contentText.length() + spoilerText.length() <= STATUS_CHARACTER_LIMIT) {
statusAlreadyInFlight = true;
int characterCount = contentText.length() + spoilerText.length();
if (characterCount > 0 && characterCount <= STATUS_CHARACTER_LIMIT) {
setStateToReadying();
readyStatus(contentText, statusVisibility, statusMarkSensitive, spoilerText);
} else if (characterCount <= 0) {
textEditor.setError(getString(R.string.error_empty));
} else {
textEditor.setError(getString(R.string.error_compose_character_limit));
}
@ -816,13 +841,13 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
private void onSendSuccess() {
Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), getString(R.string.confirmation_send), Snackbar.LENGTH_SHORT);
bar.show();
setResult(COMPOSE_SUCCESS);
finish();
}
private void onSendFailure() {
postProgress.setVisibility(View.INVISIBLE);
textEditor.setError(getString(R.string.error_generic));
statusAlreadyInFlight = false;
setStateToNotReadying();
}
private void readyStatus(final String content, final String visibility, final boolean sensitive,
@ -857,7 +882,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
@Override
protected void onCancelled() {
removeAllMediaFromQueue();
statusAlreadyInFlight = false;
setStateToNotReadying();
super.onCancelled();
}
};
@ -882,7 +907,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
readyStatus(content, visibility, sensitive, spoilerText);
}
});
statusAlreadyInFlight = false;
setStateToNotReadying();
}
private void onMediaPick() {

View file

@ -23,6 +23,8 @@ class FooterViewHolder extends RecyclerView.ViewHolder {
FooterViewHolder(View itemView) {
super(itemView);
ProgressBar progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar);
progressBar.setIndeterminate(true);
if (progressBar != null) {
progressBar.setIndeterminate(true);
}
}
}

View file

@ -66,6 +66,7 @@ import retrofit2.Response;
public class MainActivity extends BaseActivity {
private static final String TAG = "MainActivity"; // logging tag and Volley request tag
protected static int COMPOSE_RESULT = 1;
private String loggedInAccountId;
private String loggedInAccountUsername;
@ -99,7 +100,7 @@ public class MainActivity extends BaseActivity {
@Override
public void onClick(View v) {
Intent intent = new Intent(getApplicationContext(), ComposeActivity.class);
startActivity(intent);
startActivityForResult(intent, COMPOSE_RESULT);
}
});
@ -471,6 +472,17 @@ public class MainActivity extends BaseActivity {
Log.e(TAG, "Failed to fetch user info. " + exception.getMessage());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
TimelinePagerAdapter adapter = (TimelinePagerAdapter) viewPager.getAdapter();
if (adapter.getCurrentFragment() instanceof SFragment) {
((SFragment) adapter.getCurrentFragment()).onSuccessfulStatus();
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onBackPressed() {
if(drawer != null && drawer.isDrawerOpen()) {

View file

@ -42,9 +42,16 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
private static final int VIEW_TYPE_FOLLOW = 3;
enum FooterState {
EMPTY,
END,
LOADING
}
private List<Notification> notifications;
private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener;
private FooterState footerState = FooterState.END;
NotificationsAdapter(StatusActionListener statusListener,
NotificationActionListener notificationActionListener) {
@ -54,6 +61,15 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
this.notificationActionListener = notificationActionListener;
}
public void setFooterState(FooterState newFooterState) {
FooterState oldValue = footerState;
footerState = newFooterState;
if (footerState != oldValue) {
notifyItemChanged(notifications.size());
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
@ -64,8 +80,24 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
return new StatusViewHolder(view);
}
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
View view;
switch (footerState) {
default:
case LOADING:
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
break;
case END: {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer_end, parent, false);
break;
}
case EMPTY: {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer_empty, parent, false);
break;
}
}
return new FooterViewHolder(view);
}
case VIEW_TYPE_STATUS_NOTIFICATION: {

View file

@ -144,6 +144,10 @@ public class NotificationsFragment extends SFragment implements
private void sendFetchNotificationsRequest(final String fromId, String uptoId) {
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
if (fromId != null || adapter.getItemCount() <= 1) {
adapter.setFooterState(NotificationsAdapter.FooterState.LOADING);
}
listCall = api.notifications(fromId, uptoId, null);
listCall.enqueue(new Callback<List<Notification>>() {
@ -192,6 +196,11 @@ public class NotificationsFragment extends SFragment implements
} else {
adapter.update(notifications);
}
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
adapter.setFooterState(NotificationsAdapter.FooterState.EMPTY);
} else if (fromId != null) {
adapter.setFooterState(NotificationsAdapter.FooterState.END);
}
swipeRefreshLayout.setRefreshing(false);
}

View file

@ -44,9 +44,10 @@ import retrofit2.Callback;
* 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 BaseFragment {
public abstract class SFragment extends BaseFragment {
protected String loggedInAccountId;
protected String loggedInUsername;
protected static int COMPOSE_RESULT = 1;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -79,11 +80,23 @@ public class SFragment extends BaseFragment {
intent.putExtra("reply_visibility", replyVisibility);
intent.putExtra("content_warning", contentWarning);
intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0]));
startActivity(intent);
startActivityForResult(intent, COMPOSE_RESULT);
}
public void onSuccessfulStatus() {
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
onSuccessfulStatus();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
protected void reblog(final Status status, final boolean reblog,
final RecyclerView.Adapter adapter, final int position) {
final RecyclerView.Adapter adapter, final int position) {
String id = status.getActionableId();
Callback<Status> cb = new Callback<Status>() {
@ -182,8 +195,8 @@ public class SFragment extends BaseFragment {
callList.add(call);
}
protected void more(Status status, View view, final AdapterItemRemover adapter,
final int position) {
protected void more(final Status status, View view, final AdapterItemRemover adapter,
final int position) {
final String id = status.getActionableId();
final String accountId = status.getActionableStatus().account.id;
final String accountUsename = status.getActionableStatus().account.username;
@ -201,12 +214,25 @@ public class SFragment extends BaseFragment {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.status_share: {
case R.id.status_share_content: {
StringBuilder sb = new StringBuilder();
sb.append(status.account.username);
sb.append(" - ");
sb.append(status.content.toString());
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString());
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to)));
return true;
}
case R.id.status_share_link: {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_to)));
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to)));
return true;
}
case R.id.status_block: {

View file

@ -65,20 +65,43 @@ class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
notifyItemRemoved(position);
}
int insertStatus(Status status) {
int setStatus(Status status) {
if (statuses.size() > 0 && statuses.get(statusIndex).equals(status)) {
// Do not add this status on refresh, it's already in there.
statuses.set(statusIndex, status);
return statusIndex;
}
int i = statusIndex;
statuses.add(i, status);
notifyItemInserted(i);
return i;
}
void addAncestors(List<Status> ancestors) {
void setContext(List<Status> ancestors, List<Status> descendants) {
Status mainStatus = null;
// In case of refresh, remove old ancestors and descendants first. We'll remove all blindly,
// as we have no guarantee on their order to be the same as before
int old_size = statuses.size();
if (old_size > 0) {
mainStatus = statuses.get(statusIndex);
statuses.clear();
notifyItemRangeRemoved(0, old_size);
}
// Insert newly fetched ancestors
statusIndex = ancestors.size();
statuses.addAll(0, ancestors);
notifyItemRangeInserted(0, statusIndex);
}
void addDescendants(List<Status> descendants) {
if (mainStatus != null) {
// In case we needed to delete everything (which is way easier than deleting
// everything except one), re-insert the remaining status here.
statuses.add(statusIndex, mainStatus);
notifyItemInserted(statusIndex);
}
// Insert newly fetched descendants
int end = statuses.size();
statuses.addAll(descendants);
notifyItemRangeInserted(end, descendants.size());

View file

@ -30,8 +30,15 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover
private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_FOOTER = 1;
enum FooterState {
EMPTY,
END,
LOADING
}
private List<Status> statuses;
private StatusActionListener statusListener;
private FooterState footerState = FooterState.END;
TimelineAdapter(StatusActionListener statusListener) {
super();
@ -49,13 +56,37 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover
return new StatusViewHolder(view);
}
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer, viewGroup, false);
View view;
switch (footerState) {
default:
case LOADING:
view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer, viewGroup, false);
break;
case END: {
view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer_end, viewGroup, false);
break;
}
case EMPTY: {
view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer_empty, viewGroup, false);
break;
}
}
return new FooterViewHolder(view);
}
}
}
public void setFooterState(FooterState newFooterState) {
FooterState oldValue = footerState;
footerState = newFooterState;
if (footerState != oldValue) {
notifyItemChanged(statuses.size());
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < statuses.size()) {

View file

@ -214,6 +214,10 @@ public class TimelineFragment extends SFragment implements
private void sendFetchTimelineRequest(@Nullable final String fromId, @Nullable String uptoId) {
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
if (fromId != null || adapter.getItemCount() <= 1) {
adapter.setFooterState(TimelineAdapter.FooterState.LOADING);
}
Callback<List<Status>> cb = new Callback<List<Status>>() {
@Override
public void onResponse(Call<List<Status>> call, retrofit2.Response<List<Status>> response) {
@ -282,6 +286,11 @@ public class TimelineFragment extends SFragment implements
} else {
adapter.update(statuses);
}
if (statuses.size() == 0 && adapter.getItemCount() == 1) {
adapter.setFooterState(TimelineAdapter.FooterState.EMPTY);
} else if(fromId != null) {
adapter.setFooterState(TimelineAdapter.FooterState.END);
}
swipeRefreshLayout.setRefreshing(false);
}
@ -299,6 +308,14 @@ public class TimelineFragment extends SFragment implements
}
}
@Override
public void onSuccessfulStatus() {
if (kind == Kind.HOME || kind == Kind.PUBLIC_FEDERATED || kind == Kind.PUBLIC_LOCAL) {
onRefresh();
}
super.onSuccessfulStatus();
}
public void onReply(int position) {
super.reply(adapter.getItem(position));
}

View file

@ -18,12 +18,27 @@ package com.keylesspalace.tusky;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.view.ViewGroup;
class TimelinePagerAdapter extends FragmentPagerAdapter {
private Fragment currentFragment;
TimelinePagerAdapter(FragmentManager manager) {
super(manager);
}
public Fragment getCurrentFragment() {
return currentFragment;
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
if (getCurrentFragment() != object) {
currentFragment = ((Fragment) object);
}
super.setPrimaryItem(container, position, object);
}
@Override
public Fragment getItem(int i) {
switch (i) {

View file

@ -21,6 +21,7 @@ import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
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.RecyclerView;
@ -34,9 +35,11 @@ import com.keylesspalace.tusky.entity.StatusContext;
import retrofit2.Call;
import retrofit2.Callback;
public class ViewThreadFragment extends SFragment implements StatusActionListener {
public class ViewThreadFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, StatusActionListener {
private static final String TAG = "ViewThreadFragment";
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
private ThreadAdapter adapter;
private String thisThreadsStatusId;
@ -56,6 +59,9 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false);
Context context = getContext();
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
swipeRefreshLayout.setOnRefreshListener(this);
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true);
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
@ -86,7 +92,7 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
@Override
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
if (response.isSuccessful()) {
int position = adapter.insertStatus(response.body());
int position = adapter.setStatus(response.body());
recyclerView.scrollToPosition(position);
} else {
onThreadRequestFailure(id);
@ -109,10 +115,10 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
@Override
public void onResponse(Call<StatusContext> call, retrofit2.Response<StatusContext> response) {
if (response.isSuccessful()) {
swipeRefreshLayout.setRefreshing(false);
StatusContext context = response.body();
adapter.addAncestors(context.ancestors);
adapter.addDescendants(context.descendants);
adapter.setContext(context.ancestors, context.descendants);
} else {
onThreadRequestFailure(id);
}
@ -128,6 +134,7 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
private void onThreadRequestFailure(final String id) {
View view = getView();
swipeRefreshLayout.setRefreshing(false);
if (view != null) {
Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry, new View.OnClickListener() {
@ -143,6 +150,17 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
}
}
public void onRefresh() {
sendStatusRequest(thisThreadsStatusId);
sendThreadRequest(thisThreadsStatusId);
}
@Override
public void onSuccessfulStatus() {
onRefresh();
super.onSuccessfulStatus();
}
public void onReply(int position) {
super.reply(adapter.getItem(position));
}