Add empty state for conversation list

This commit is contained in:
Moxie Marlinspike 2017-11-12 12:39:22 -08:00
parent 90ff0e58b0
commit 3097c2855e
9 changed files with 155 additions and 72 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -3,11 +3,30 @@
<android.support.design.widget.CoordinatorLayout <android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:orientation="vertical"> android:orientation="vertical">
<FrameLayout android:id="@+id/empty_state"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<ImageView android:id="@+id/empty"
android:src="@drawable/conversation_list_empty_state"
android:scaleType="centerCrop"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView android:text="@string/conversation_list_fragment__give_your_inbox_something_to_write_home_about_get_started_by_messaging_a_friend"
android:textSize="20sp"
android:padding="16dp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</FrameLayout>
<LinearLayout android:layout_width="match_parent" <LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
@ -28,7 +47,8 @@
</LinearLayout> </LinearLayout>
<android.support.design.widget.FloatingActionButton
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -38,4 +58,5 @@
android:focusable="true" android:focusable="true"
android:contentDescription="@string/conversation_list_fragment__fab_content_description"/> android:contentDescription="@string/conversation_list_fragment__fab_content_description"/>
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>

View File

@ -1490,6 +1490,7 @@
<string name="recipient_preference_activity__shared_media">Shared media</string> <string name="recipient_preference_activity__shared_media">Shared media</string>
<string name="registration_activity__verify_your_number">Verify Your Number</string> <string name="registration_activity__verify_your_number">Verify Your Number</string>
<string name="registration_activity__please_enter_your_mobile_number_to_receive_a_verification_code_carrier_rates_may_apply">Please enter your mobile number to receive a verification code. Carrier rates may apply.</string> <string name="registration_activity__please_enter_your_mobile_number_to_receive_a_verification_code_carrier_rates_may_apply">Please enter your mobile number to receive a verification code. Carrier rates may apply.</string>
<string name="conversation_list_fragment__give_your_inbox_something_to_write_home_about_get_started_by_messaging_a_friend">Give your inbox something to write home about. Get started by messaging a friend.</string>
<!-- EOF --> <!-- EOF -->

View File

@ -16,9 +16,9 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.database.Cursor; import android.database.Cursor;
@ -31,7 +31,6 @@ import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
@ -48,11 +47,11 @@ import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.thoughtcrime.securesms.ConversationListAdapter.ItemClickListener; import org.thoughtcrime.securesms.ConversationListAdapter.ItemClickListener;
import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator; import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
import org.thoughtcrime.securesms.components.reminder.DefaultSmsReminder; import org.thoughtcrime.securesms.components.reminder.DefaultSmsReminder;
import org.thoughtcrime.securesms.components.reminder.DozeReminder; import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder; import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
@ -60,7 +59,6 @@ import org.thoughtcrime.securesms.components.reminder.OutdatedBuildReminder;
import org.thoughtcrime.securesms.components.reminder.PushRegistrationReminder; import org.thoughtcrime.securesms.components.reminder.PushRegistrationReminder;
import org.thoughtcrime.securesms.components.reminder.Reminder; import org.thoughtcrime.securesms.components.reminder.Reminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView; import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.components.reminder.ReminderView.OnDismissListener;
import org.thoughtcrime.securesms.components.reminder.ShareReminder; import org.thoughtcrime.securesms.components.reminder.ShareReminder;
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder; import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -92,7 +90,8 @@ public class ConversationListFragment extends Fragment
private ActionMode actionMode; private ActionMode actionMode;
private RecyclerView list; private RecyclerView list;
private ReminderView reminderView; private ReminderView reminderView;
private FloatingActionButton fab; private View emptyState;
private PulsingFloatingActionButton fab;
private Locale locale; private Locale locale;
private String queryFilter = ""; private String queryFilter = "";
private boolean archive; private boolean archive;
@ -112,16 +111,12 @@ public class ConversationListFragment extends Fragment
reminderView = ViewUtil.findById(view, R.id.reminder); reminderView = ViewUtil.findById(view, R.id.reminder);
list = ViewUtil.findById(view, R.id.list); list = ViewUtil.findById(view, R.id.list);
fab = ViewUtil.findById(view, R.id.fab); fab = ViewUtil.findById(view, R.id.fab);
emptyState = ViewUtil.findById(view, R.id.empty_state);
if (archive) fab.setVisibility(View.GONE); if (archive) fab.setVisibility(View.GONE);
else fab.setVisibility(View.VISIBLE); else fab.setVisibility(View.VISIBLE);
reminderView.setOnDismissListener(new OnDismissListener() { reminderView.setOnDismissListener(this::updateReminders);
@Override
public void onDismiss() {
updateReminders();
}
});
list.setHasFixedSize(true); list.setHasFixedSize(true);
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
@ -137,12 +132,7 @@ public class ConversationListFragment extends Fragment
super.onActivityCreated(bundle); super.onActivityCreated(bundle);
setHasOptionsMenu(true); setHasOptionsMenu(true);
fab.setOnClickListener(new OnClickListener() { fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class)));
@Override
public void onClick(View v) {
startActivity(new Intent(getActivity(), NewConversationActivity.class));
}
});
initializeListAdapter(); initializeListAdapter();
} }
@ -154,6 +144,13 @@ public class ConversationListFragment extends Fragment
list.getAdapter().notifyDataSetChanged(); list.getAdapter().notifyDataSetChanged();
} }
@Override
public void onPause() {
super.onPause();
fab.stopPulse();
}
public ConversationListAdapter getListAdapter() { public ConversationListAdapter getListAdapter() {
return (ConversationListAdapter) list.getAdapter(); return (ConversationListAdapter) list.getAdapter();
} }
@ -169,6 +166,7 @@ public class ConversationListFragment extends Fragment
} }
} }
@SuppressLint("StaticFieldLeak")
private void updateReminders() { private void updateReminders() {
reminderView.hide(); reminderView.hide();
new AsyncTask<Context, Void, Optional<? extends Reminder>>() { new AsyncTask<Context, Void, Optional<? extends Reminder>>() {
@ -206,6 +204,7 @@ public class ConversationListFragment extends Fragment
getLoaderManager().restartLoader(0, null, this); getLoaderManager().restartLoader(0, null, this);
} }
@SuppressLint("StaticFieldLeak")
private void handleArchiveAllSelected() { private void handleArchiveAllSelected() {
final Set<Long> selectedConversations = new HashSet<>(getListAdapter().getBatchSelections()); final Set<Long> selectedConversations = new HashSet<>(getListAdapter().getBatchSelections());
final boolean archive = this.archive; final boolean archive = this.archive;
@ -252,6 +251,7 @@ public class ConversationListFragment extends Fragment
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
@SuppressLint("StaticFieldLeak")
private void handleDeleteAllSelected() { private void handleDeleteAllSelected() {
int conversationsCount = getListAdapter().getBatchSelections().size(); int conversationsCount = getListAdapter().getBatchSelections().size();
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
@ -262,9 +262,7 @@ public class ConversationListFragment extends Fragment
conversationsCount, conversationsCount)); conversationsCount, conversationsCount));
alert.setCancelable(true); alert.setCancelable(true);
alert.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { alert.setPositiveButton(R.string.delete, (dialog, which) -> {
@Override
public void onClick(DialogInterface dialog, int which) {
final Set<Long> selectedConversations = (getListAdapter()) final Set<Long> selectedConversations = (getListAdapter())
.getBatchSelections(); .getBatchSelections();
@ -297,7 +295,6 @@ public class ConversationListFragment extends Fragment
} }
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
}
}); });
alert.setNegativeButton(android.R.string.cancel, null); alert.setNegativeButton(android.R.string.cancel, null);
@ -307,7 +304,7 @@ public class ConversationListFragment extends Fragment
private void handleSelectAllThreads() { private void handleSelectAllThreads() {
getListAdapter().selectAllThreads(); getListAdapter().selectAllThreads();
actionMode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, actionMode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount,
getListAdapter().getBatchSelections().size())); String.valueOf(getListAdapter().getBatchSelections().size())));
} }
private void handleCreateConversation(long threadId, Recipient recipient, int distributionType, long lastSeen) { private void handleCreateConversation(long threadId, Recipient recipient, int distributionType, long lastSeen) {
@ -321,6 +318,16 @@ public class ConversationListFragment extends Fragment
@Override @Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) { public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) {
if (cursor == null || cursor.getCount() <= 0) {
list.setVisibility(View.INVISIBLE);
emptyState.setVisibility(View.VISIBLE);
fab.startPulse(3 * 1000);
} else {
list.setVisibility(View.VISIBLE);
emptyState.setVisibility(View.GONE);
fab.stopPulse();
}
getListAdapter().changeCursor(cursor); getListAdapter().changeCursor(cursor);
} }
@ -342,7 +349,7 @@ public class ConversationListFragment extends Fragment
actionMode.finish(); actionMode.finish();
} else { } else {
actionMode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, actionMode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount,
adapter.getBatchSelections().size())); String.valueOf(adapter.getBatchSelections().size())));
} }
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
@ -378,7 +385,7 @@ public class ConversationListFragment extends Fragment
inflater.inflate(R.menu.conversation_list_batch, menu); inflater.inflate(R.menu.conversation_list_batch, menu);
mode.setTitle(R.string.conversation_fragment_cab__batch_selection_mode); mode.setTitle(R.string.conversation_fragment_cab__batch_selection_mode);
mode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, 1)); mode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, "1"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar)); getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
@ -418,7 +425,7 @@ public class ConversationListFragment extends Fragment
private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback { private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback {
public ArchiveListenerCallback() { ArchiveListenerCallback() {
super(0, ItemTouchHelper.RIGHT); super(0, ItemTouchHelper.RIGHT);
} }
@ -443,6 +450,7 @@ public class ConversationListFragment extends Fragment
return super.getSwipeDirs(recyclerView, viewHolder); return super.getSwipeDirs(recyclerView, viewHolder);
} }
@SuppressLint("StaticFieldLeak")
@Override @Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
final long threadId = ((ConversationListItem)viewHolder.itemView).getThreadId(); final long threadId = ((ConversationListItem)viewHolder.itemView).getThreadId();
@ -524,11 +532,9 @@ public class ConversationListFragment extends Fragment
p); p);
} }
if (Build.VERSION.SDK_INT >= 11) {
float alpha = 1.0f - Math.abs(dX) / (float) viewHolder.itemView.getWidth(); float alpha = 1.0f - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
viewHolder.itemView.setAlpha(alpha); viewHolder.itemView.setAlpha(alpha);
viewHolder.itemView.setTranslationX(dX); viewHolder.itemView.setTranslationX(dX);
}
} else { } else {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);

View File

@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.components.registration;
import android.animation.Animator;
import android.content.Context;
import android.support.design.widget.FloatingActionButton;
import android.util.AttributeSet;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
public class PulsingFloatingActionButton extends FloatingActionButton {
private boolean pulsing;
public PulsingFloatingActionButton(Context context) {
super(context);
}
public PulsingFloatingActionButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PulsingFloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void startPulse(long periodMillis) {
if (!pulsing) {
pulsing = true;
pulse(periodMillis);
}
}
public void stopPulse() {
pulsing = false;
}
private void pulse(long periodMillis) {
if (!pulsing) return;
this.animate().scaleX(1.2f).scaleY(1.2f).setDuration(150).setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
clearAnimation();
animate().scaleX(1.0f).scaleY(1.0f).setDuration(150).setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
PulsingFloatingActionButton.this.postDelayed(() -> pulse(periodMillis), periodMillis);
}
}).start();
}
}).start();
}
}