diff --git a/res/drawable-hdpi/conversation_list_empty_state.png b/res/drawable-hdpi/conversation_list_empty_state.png new file mode 100644 index 0000000000..2d67a3fce8 Binary files /dev/null and b/res/drawable-hdpi/conversation_list_empty_state.png differ diff --git a/res/drawable-mdpi/conversation_list_empty_state.png b/res/drawable-mdpi/conversation_list_empty_state.png new file mode 100644 index 0000000000..e62bf67765 Binary files /dev/null and b/res/drawable-mdpi/conversation_list_empty_state.png differ diff --git a/res/drawable-xhdpi/conversation_list_empty_state.png b/res/drawable-xhdpi/conversation_list_empty_state.png new file mode 100644 index 0000000000..7d225be9d5 Binary files /dev/null and b/res/drawable-xhdpi/conversation_list_empty_state.png differ diff --git a/res/drawable-xxhdpi/conversation_list_empty_state.png b/res/drawable-xxhdpi/conversation_list_empty_state.png new file mode 100644 index 0000000000..aff1f9a7d0 Binary files /dev/null and b/res/drawable-xxhdpi/conversation_list_empty_state.png differ diff --git a/res/drawable-xxxhdpi/conversation_list_empty_state.png b/res/drawable-xxxhdpi/conversation_list_empty_state.png new file mode 100644 index 0000000000..12535ab87d Binary files /dev/null and b/res/drawable-xxxhdpi/conversation_list_empty_state.png differ diff --git a/res/layout/conversation_list_fragment.xml b/res/layout/conversation_list_fragment.xml index 01d2eb5d55..3e459b37f1 100644 --- a/res/layout/conversation_list_fragment.xml +++ b/res/layout/conversation_list_fragment.xml @@ -1,12 +1,31 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical"> + + + + + + + + - + diff --git a/res/values/strings.xml b/res/values/strings.xml index 7544d003cb..c47a24118e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1490,6 +1490,7 @@ Shared media Verify Your Number Please enter your mobile number to receive a verification code. Carrier rates may apply. + Give your inbox something to write home about. Get started by messaging a friend. diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java index 81f6794d3c..e8b2f74f6c 100644 --- a/src/org/thoughtcrime/securesms/ConversationListFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationListFragment.java @@ -16,9 +16,9 @@ */ package org.thoughtcrime.securesms; +import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.res.TypedArray; import android.database.Cursor; @@ -31,7 +31,6 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; -import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; @@ -48,11 +47,11 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import org.thoughtcrime.securesms.ConversationListAdapter.ItemClickListener; 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.DozeReminder; 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.Reminder; 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.SystemSmsImportReminder; import org.thoughtcrime.securesms.crypto.MasterSecret; @@ -88,14 +86,15 @@ public class ConversationListFragment extends Fragment public static final String ARCHIVE = "archive"; - private MasterSecret masterSecret; - private ActionMode actionMode; - private RecyclerView list; - private ReminderView reminderView; - private FloatingActionButton fab; - private Locale locale; - private String queryFilter = ""; - private boolean archive; + private MasterSecret masterSecret; + private ActionMode actionMode; + private RecyclerView list; + private ReminderView reminderView; + private View emptyState; + private PulsingFloatingActionButton fab; + private Locale locale; + private String queryFilter = ""; + private boolean archive; @Override public void onCreate(Bundle icicle) { @@ -112,16 +111,12 @@ public class ConversationListFragment extends Fragment reminderView = ViewUtil.findById(view, R.id.reminder); list = ViewUtil.findById(view, R.id.list); fab = ViewUtil.findById(view, R.id.fab); + emptyState = ViewUtil.findById(view, R.id.empty_state); if (archive) fab.setVisibility(View.GONE); else fab.setVisibility(View.VISIBLE); - reminderView.setOnDismissListener(new OnDismissListener() { - @Override - public void onDismiss() { - updateReminders(); - } - }); + reminderView.setOnDismissListener(this::updateReminders); list.setHasFixedSize(true); list.setLayoutManager(new LinearLayoutManager(getActivity())); @@ -137,12 +132,7 @@ public class ConversationListFragment extends Fragment super.onActivityCreated(bundle); setHasOptionsMenu(true); - fab.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - startActivity(new Intent(getActivity(), NewConversationActivity.class)); - } - }); + fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class))); initializeListAdapter(); } @@ -154,6 +144,13 @@ public class ConversationListFragment extends Fragment list.getAdapter().notifyDataSetChanged(); } + @Override + public void onPause() { + super.onPause(); + + fab.stopPulse(); + } + public ConversationListAdapter getListAdapter() { return (ConversationListAdapter) list.getAdapter(); } @@ -169,6 +166,7 @@ public class ConversationListFragment extends Fragment } } + @SuppressLint("StaticFieldLeak") private void updateReminders() { reminderView.hide(); new AsyncTask>() { @@ -206,6 +204,7 @@ public class ConversationListFragment extends Fragment getLoaderManager().restartLoader(0, null, this); } + @SuppressLint("StaticFieldLeak") private void handleArchiveAllSelected() { final Set selectedConversations = new HashSet<>(getListAdapter().getBatchSelections()); final boolean archive = this.archive; @@ -252,6 +251,7 @@ public class ConversationListFragment extends Fragment }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + @SuppressLint("StaticFieldLeak") private void handleDeleteAllSelected() { int conversationsCount = getListAdapter().getBatchSelections().size(); AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); @@ -262,41 +262,38 @@ public class ConversationListFragment extends Fragment conversationsCount, conversationsCount)); alert.setCancelable(true); - alert.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - final Set selectedConversations = (getListAdapter()) - .getBatchSelections(); + alert.setPositiveButton(R.string.delete, (dialog, which) -> { + final Set selectedConversations = (getListAdapter()) + .getBatchSelections(); - if (!selectedConversations.isEmpty()) { - new AsyncTask() { - private ProgressDialog dialog; + if (!selectedConversations.isEmpty()) { + new AsyncTask() { + private ProgressDialog dialog; - @Override - protected void onPreExecute() { - dialog = ProgressDialog.show(getActivity(), - getActivity().getString(R.string.ConversationListFragment_deleting), - getActivity().getString(R.string.ConversationListFragment_deleting_selected_conversations), - true, false); + @Override + protected void onPreExecute() { + dialog = ProgressDialog.show(getActivity(), + getActivity().getString(R.string.ConversationListFragment_deleting), + getActivity().getString(R.string.ConversationListFragment_deleting_selected_conversations), + true, false); + } + + @Override + protected Void doInBackground(Void... params) { + DatabaseFactory.getThreadDatabase(getActivity()).deleteConversations(selectedConversations); + MessageNotifier.updateNotification(getActivity(), masterSecret); + return null; + } + + @Override + protected void onPostExecute(Void result) { + dialog.dismiss(); + if (actionMode != null) { + actionMode.finish(); + actionMode = null; } - - @Override - protected Void doInBackground(Void... params) { - DatabaseFactory.getThreadDatabase(getActivity()).deleteConversations(selectedConversations); - MessageNotifier.updateNotification(getActivity(), masterSecret); - return null; - } - - @Override - protected void onPostExecute(Void result) { - dialog.dismiss(); - if (actionMode != null) { - actionMode.finish(); - actionMode = null; - } - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } }); @@ -307,7 +304,7 @@ public class ConversationListFragment extends Fragment private void handleSelectAllThreads() { getListAdapter().selectAllThreads(); 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) { @@ -321,6 +318,16 @@ public class ConversationListFragment extends Fragment @Override public void onLoadFinished(Loader 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); } @@ -342,7 +349,7 @@ public class ConversationListFragment extends Fragment actionMode.finish(); } else { actionMode.setSubtitle(getString(R.string.conversation_fragment_cab__batch_selection_amount, - adapter.getBatchSelections().size())); + String.valueOf(adapter.getBatchSelections().size()))); } adapter.notifyDataSetChanged(); @@ -378,7 +385,7 @@ public class ConversationListFragment extends Fragment inflater.inflate(R.menu.conversation_list_batch, menu); 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) { 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 { - public ArchiveListenerCallback() { + ArchiveListenerCallback() { super(0, ItemTouchHelper.RIGHT); } @@ -443,6 +450,7 @@ public class ConversationListFragment extends Fragment return super.getSwipeDirs(recyclerView, viewHolder); } + @SuppressLint("StaticFieldLeak") @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { final long threadId = ((ConversationListItem)viewHolder.itemView).getThreadId(); @@ -524,11 +532,9 @@ public class ConversationListFragment extends Fragment p); } - if (Build.VERSION.SDK_INT >= 11) { - float alpha = 1.0f - Math.abs(dX) / (float) viewHolder.itemView.getWidth(); - viewHolder.itemView.setAlpha(alpha); - viewHolder.itemView.setTranslationX(dX); - } + float alpha = 1.0f - Math.abs(dX) / (float) viewHolder.itemView.getWidth(); + viewHolder.itemView.setAlpha(alpha); + viewHolder.itemView.setTranslationX(dX); } else { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); diff --git a/src/org/thoughtcrime/securesms/components/registration/PulsingFloatingActionButton.java b/src/org/thoughtcrime/securesms/components/registration/PulsingFloatingActionButton.java new file mode 100644 index 0000000000..9277f1eb87 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/registration/PulsingFloatingActionButton.java @@ -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(); + } + +}