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();
+ }
+
+}