From d70c33d20f6df844f1c7a8d42c93702511b3c7a1 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 28 May 2020 09:27:00 -0400 Subject: [PATCH] Add support for mark as unread. --- .../BindableConversationListItem.java | 8 +- .../components/ContactFilterToolbar.java | 3 +- .../ConversationListAdapter.java | 37 ++++-- .../ConversationListFragment.java | 78 +++++++++-- .../ConversationListItem.java | 24 ++-- .../securesms/database/ThreadDatabase.java | 125 ++++++++++++++---- .../database/model/ThreadRecord.java | 29 +++- .../messages/InitialMessageRetriever.java | 1 - .../util/views/DarkOverflowToolbar.java | 39 ++++++ .../res/layout/conversation_list_fragment.xml | 4 +- .../main/res/menu/conversation_list_batch.xml | 32 +++-- app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 2 +- 13 files changed, 302 insertions(+), 83 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/views/DarkOverflowToolbar.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java index ec3a45cc78..71c8faef5a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationListItem.java @@ -10,8 +10,8 @@ import java.util.Set; public interface BindableConversationListItem extends Unbindable { - public void bind(@NonNull ThreadRecord thread, - @NonNull GlideRequests glideRequests, @NonNull Locale locale, - @NonNull Set typingThreads, - @NonNull Set selectedThreads, boolean batchMode); + void bind(@NonNull ThreadRecord thread, + @NonNull GlideRequests glideRequests, @NonNull Locale locale, + @NonNull Set typingThreads, + @NonNull Set selectedThreads, boolean batchMode); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java b/app/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java index 91336d6ad7..b8f32d0c7f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ContactFilterToolbar.java @@ -21,8 +21,9 @@ import android.widget.LinearLayout; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.util.views.DarkOverflowToolbar; -public class ContactFilterToolbar extends Toolbar { +public class ContactFilterToolbar extends DarkOverflowToolbar { private OnFilterChangedListener listener; private EditText searchText; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java index 53c0491f7d..9481efb44b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java @@ -25,6 +25,9 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.annimon.stream.Collectors; +import com.annimon.stream.Stream; + import org.thoughtcrime.securesms.BindableConversationListItem; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; @@ -37,8 +40,10 @@ import org.thoughtcrime.securesms.util.Conversions; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Locale; +import java.util.Map; import java.util.Set; /** @@ -59,9 +64,9 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter batchSet = Collections.synchronizedSet(new HashSet()); - private boolean batchMode = false; - private final Set typingSet = new HashSet<>(); + private final Map batchSet = Collections.synchronizedMap(new HashMap<>()); + private boolean batchMode = false; + private final Set typingSet = new HashSet<>(); protected static class ViewHolder extends RecyclerView.ViewHolder { public ViewHolder(final @NonNull V itemView) @@ -143,7 +148,7 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter getBatchSelections() { - return batchSet; + @NonNull Set getBatchSelectionIds() { + return batchSet.keySet(); + } + + @NonNull Set getBatchSelection() { + return new HashSet<>(batchSet.values()); } void initializeBatchMode(boolean toggle) { @@ -193,8 +202,10 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds()); + + SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> { + List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(selectedConversations, false); + + ApplicationDependencies.getMessageNotifier().updateNotification(context); + MarkReadReceiver.process(context, messageIds); + + return null; + }, none -> { + if (actionMode != null) { + actionMode.finish(); + actionMode = null; + } + }); + } + + private void handleMarkSelectedAsUnread() { + Context context = requireContext(); + Set selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds()); + + SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> { + DatabaseFactory.getThreadDatabase(context).setForcedUnread(selectedConversations); + StorageSyncHelper.scheduleSyncForDataChange(); + return null; + }, none -> { + if (actionMode != null) { + actionMode.finish(); + actionMode = null; + } + }); + } + private void handleInvite() { getNavigator().goToInvite(); } @@ -603,7 +643,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana @SuppressLint("StaticFieldLeak") private void handleArchiveAllSelected() { - Set selectedConversations = new HashSet<>(defaultAdapter.getBatchSelections()); + Set selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds()); int count = selectedConversations.size(); String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count); @@ -642,7 +682,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana @SuppressLint("StaticFieldLeak") private void handleDeleteAllSelected() { - int conversationsCount = defaultAdapter.getBatchSelections().size(); + int conversationsCount = defaultAdapter.getBatchSelectionIds().size(); AlertDialog.Builder alert = new AlertDialog.Builder(getActivity()); alert.setIconAttribute(R.attr.dialog_alert_icon); alert.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationListFragment_delete_selected_conversations, @@ -652,7 +692,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana alert.setCancelable(true); alert.setPositiveButton(R.string.delete, (dialog, which) -> { - final Set selectedConversations = defaultAdapter.getBatchSelections(); + final Set selectedConversations = defaultAdapter.getBatchSelectionIds(); if (!selectedConversations.isEmpty()) { new AsyncTask() { @@ -691,7 +731,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana private void handleSelectAllThreads() { defaultAdapter.selectAllThreads(); - actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelections().size())); + actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size())); } private void handleCreateConversation(long threadId, Recipient recipient, int distributionType) { @@ -732,12 +772,13 @@ public class ConversationListFragment extends MainFragment implements LoaderMana handleCreateConversation(item.getThreadId(), item.getRecipient(), item.getDistributionType()); } else { ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter(); - adapter.toggleThreadInBatchSet(item.getThreadId()); + adapter.toggleThreadInBatchSet(item.getThread()); - if (adapter.getBatchSelections().size() == 0) { + if (adapter.getBatchSelectionIds().size() == 0) { actionMode.finish(); } else { - actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelections().size())); + actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size())); + setCorrectMenuVisibility(actionMode.getMenu()); } adapter.notifyDataSetChanged(); @@ -749,7 +790,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this); defaultAdapter.initializeBatchMode(true); - defaultAdapter.toggleThreadInBatchSet(item.getThreadId()); + defaultAdapter.toggleThreadInBatchSet(item.getThread()); defaultAdapter.notifyDataSetChanged(); } @@ -781,15 +822,18 @@ public class ConversationListFragment extends MainFragment implements LoaderMana @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + setCorrectMenuVisibility(menu); return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { - case R.id.menu_select_all: handleSelectAllThreads(); return true; - case R.id.menu_delete_selected: handleDeleteAllSelected(); return true; - case R.id.menu_archive_selected: handleArchiveAllSelected(); return true; + case R.id.menu_select_all: handleSelectAllThreads(); return true; + case R.id.menu_delete_selected: handleDeleteAllSelected(); return true; + case R.id.menu_archive_selected: handleArchiveAllSelected(); return true; + case R.id.menu_mark_as_read: handleMarkSelectedAsRead(); return true; + case R.id.menu_mark_as_unread: handleMarkSelectedAsUnread(); return true; } return false; @@ -830,6 +874,18 @@ public class ConversationListFragment extends MainFragment implements LoaderMana closeSearchIfOpen(); } + private void setCorrectMenuVisibility(@NonNull Menu menu) { + boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(thread -> !thread.isRead()); + + if (hasUnread) { + menu.findItem(R.id.menu_mark_as_unread).setVisible(false); + menu.findItem(R.id.menu_mark_as_read).setVisible(true); + } else { + menu.findItem(R.id.menu_mark_as_unread).setVisible(true); + menu.findItem(R.id.menu_mark_as_read).setVisible(false); + } + } + protected @IdRes int getToolbarRes() { return R.id.toolbar; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index de2de92aab..18be1b02a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -170,7 +170,7 @@ public class ConversationListItem extends RelativeLayout this.fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), name, highlightSubstring)); } else { - this.fromView.setText(recipient.get(), unreadCount == 0); + this.fromView.setText(recipient.get(), thread.isRead()); } if (typingThreads.contains(threadId)) { @@ -190,17 +190,17 @@ public class ConversationListItem extends RelativeLayout groupAddedBy.observeForever(groupAddedByObserver); } - this.subjectView.setTypeface(unreadCount == 0 ? LIGHT_TYPEFACE : BOLD_TYPEFACE); - this.subjectView.setTextColor(unreadCount == 0 ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color) - : ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color)); + this.subjectView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE); + this.subjectView.setTextColor(thread.isRead() ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_subject_color) + : ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color)); } if (thread.getDate() > 0) { CharSequence date = DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, thread.getDate()); dateView.setText(date); - dateView.setTypeface(unreadCount == 0 ? LIGHT_TYPEFACE : BOLD_TYPEFACE); - dateView.setTextColor(unreadCount == 0 ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_date_color) - : ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color)); + dateView.setTypeface(thread.isRead() ? LIGHT_TYPEFACE : BOLD_TYPEFACE); + dateView.setTextColor(thread.isRead() ? ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_date_color) + : ThemeUtil.getThemedColor(getContext(), R.attr.conversation_list_item_unread_color)); } if (thread.isArchived()) { @@ -292,7 +292,7 @@ public class ConversationListItem extends RelativeLayout private void setBatchMode(boolean batchMode) { this.batchMode = batchMode; - setSelected(batchMode && selectedThreads.contains(threadId)); + setSelected(batchMode && selectedThreads.contains(thread.getThreadId())); } public Recipient getRecipient() { @@ -303,6 +303,10 @@ public class ConversationListItem extends RelativeLayout return threadId; } + public @NonNull ThreadRecord getThread() { + return thread; + } + public int getUnreadCount() { return unreadCount; } @@ -368,12 +372,12 @@ public class ConversationListItem extends RelativeLayout } private void setUnreadIndicator(ThreadRecord thread) { - if (thread.isOutgoing() || thread.getUnreadCount() == 0) { + if ((thread.isOutgoing() && !thread.isForcedUnread()) || thread.isRead()) { unreadIndicator.setVisibility(View.GONE); return; } - unreadIndicator.setText(String.valueOf(unreadCount)); + unreadIndicator.setText(unreadCount > 0 ? String.valueOf(unreadCount) : " "); unreadIndicator.setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index b2c3785494..af8bdf88c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -56,12 +56,14 @@ import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import java.io.Closeable; import java.io.IOException; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -92,17 +94,27 @@ public class ThreadDatabase extends Database { public static final String LAST_SEEN = "last_seen"; public static final String HAS_SENT = "has_sent"; - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + - ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " + - MESSAGE_COUNT + " INTEGER DEFAULT 0, " + RECIPIENT_ID + " INTEGER, " + SNIPPET + " TEXT, " + - SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " + - TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " + - SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " + - SNIPPET_CONTENT_TYPE + " TEXT DEFAULT NULL, " + SNIPPET_EXTRAS + " TEXT DEFAULT NULL, " + - ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " + - DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + EXPIRES_IN + " INTEGER DEFAULT 0, " + - LAST_SEEN + " INTEGER DEFAULT 0, " + HAS_SENT + " INTEGER DEFAULT 0, " + - READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNREAD_COUNT + " INTEGER DEFAULT 0);"; + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + + DATE + " INTEGER DEFAULT 0, " + + MESSAGE_COUNT + " INTEGER DEFAULT 0, " + + RECIPIENT_ID + " INTEGER, " + + SNIPPET + " TEXT, " + + SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + + READ + " INTEGER DEFAULT " + ReadStatus.READ.serialize() + ", " + + TYPE + " INTEGER DEFAULT 0, " + + ERROR + " INTEGER DEFAULT 0, " + + SNIPPET_TYPE + " INTEGER DEFAULT 0, " + + SNIPPET_URI + " TEXT DEFAULT NULL, " + + SNIPPET_CONTENT_TYPE + " TEXT DEFAULT NULL, " + + SNIPPET_EXTRAS + " TEXT DEFAULT NULL, " + + ARCHIVED + " INTEGER DEFAULT 0, " + + STATUS + " INTEGER DEFAULT 0, " + + DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + + EXPIRES_IN + " INTEGER DEFAULT 0, " + + LAST_SEEN + " INTEGER DEFAULT 0, " + + HAS_SENT + " INTEGER DEFAULT 0, " + + READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + + UNREAD_COUNT + " INTEGER DEFAULT 0);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");", @@ -280,7 +292,7 @@ public class ThreadDatabase extends Database { public List setAllThreadsRead() { SQLiteDatabase db = databaseHelper.getWritableDatabase(); ContentValues contentValues = new ContentValues(1); - contentValues.put(READ, 1); + contentValues.put(READ, ReadStatus.READ.serialize()); contentValues.put(UNREAD_COUNT, 0); db.update(TABLE_NAME, contentValues, null, null); @@ -310,32 +322,69 @@ public class ThreadDatabase extends Database { } public List setRead(long threadId, boolean lastSeen) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(READ, 1); - contentValues.put(UNREAD_COUNT, 0); - - if (lastSeen) { - contentValues.put(LAST_SEEN, System.currentTimeMillis()); - } + return setRead(Collections.singletonList(threadId), lastSeen); + } + public List setRead(Collection threadIds, boolean lastSeen) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""}); + List smsRecords = new LinkedList<>(); + List mmsRecords = new LinkedList<>(); - final List smsRecords = DatabaseFactory.getSmsDatabase(context).setMessagesRead(threadId); - final List mmsRecords = DatabaseFactory.getMmsDatabase(context).setMessagesRead(threadId); + db.beginTransaction(); - DatabaseFactory.getSmsDatabase(context).setReactionsSeen(threadId); - DatabaseFactory.getMmsDatabase(context).setReactionsSeen(threadId); + try { + ContentValues contentValues = new ContentValues(2); + contentValues.put(READ, ReadStatus.READ.serialize()); + contentValues.put(UNREAD_COUNT, 0); + + if (lastSeen) { + contentValues.put(LAST_SEEN, System.currentTimeMillis()); + } + + for (long threadId : threadIds) { + db.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{threadId + ""}); + + smsRecords.addAll(DatabaseFactory.getSmsDatabase(context).setMessagesRead(threadId)); + mmsRecords.addAll(DatabaseFactory.getMmsDatabase(context).setMessagesRead(threadId)); + + DatabaseFactory.getSmsDatabase(context).setReactionsSeen(threadId); + DatabaseFactory.getMmsDatabase(context).setReactionsSeen(threadId); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } notifyConversationListListeners(); - return Util.concatenatedList(smsRecords, mmsRecords); } + public void setForcedUnread(@NonNull Collection threadIds) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + db.beginTransaction(); + try { + ContentValues contentValues = new ContentValues(); + contentValues.put(READ, ReadStatus.FORCED_UNREAD.serialize()); + + for (long threadId : threadIds) { + db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] { String.valueOf(threadId) }); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + notifyConversationListListeners(); + } + + public void incrementUnread(long threadId, int amount) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); - db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " + + db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = " + ReadStatus.UNREAD.serialize() + ", " + UNREAD_COUNT + " = " + UNREAD_COUNT + " + ? WHERE " + ID + " = ?", new String[] {String.valueOf(amount), String.valueOf(threadId)}); @@ -882,6 +931,7 @@ public class ThreadDatabase extends Database { .setContentType(cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET_CONTENT_TYPE))) .setCount(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT))) .setUnreadCount(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.UNREAD_COUNT))) + .setForcedUnread(cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.READ)) == ReadStatus.FORCED_UNREAD.serialize()) .setExtra(extra) .build(); } @@ -990,4 +1040,27 @@ public class ThreadDatabase extends Database { return groupAddedBy; } } + + private enum ReadStatus { + READ(1), UNREAD(0), FORCED_UNREAD(2); + + private final int value; + + ReadStatus(int value) { + this.value = value; + } + + public static ReadStatus deserialize(int value) { + for (ReadStatus status : ReadStatus.values()) { + if (status.value == value) { + return status; + } + } + throw new IllegalArgumentException("No matching status for value " + value); + } + + public int serialize() { + return value; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index 4dda834a22..b85ff41d83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -24,11 +24,14 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase.Extra; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.libsignal.util.guava.Preconditions; +import java.util.Objects; + /** * Represents an entry in the {@link org.thoughtcrime.securesms.database.ThreadDatabase}. */ @@ -47,6 +50,7 @@ public final class ThreadRecord { private final Extra extra; private final long count; private final int unreadCount; + private final boolean forcedUnread; private final int distributionType; private final boolean archived; private final long expiresIn; @@ -66,6 +70,7 @@ public final class ThreadRecord { this.extra = builder.extra; this.count = builder.count; this.unreadCount = builder.unreadCount; + this.forcedUnread = builder.forcedUnread; this.distributionType = builder.distributionType; this.archived = builder.archived; this.expiresIn = builder.expiresIn; @@ -104,6 +109,14 @@ public final class ThreadRecord { return unreadCount; } + public boolean isForcedUnread() { + return forcedUnread; + } + + public boolean isRead() { + return unreadCount == 0 && !forcedUnread; + } + public long getDate() { return date; } @@ -188,6 +201,7 @@ public final class ThreadRecord { private Extra extra; private long count; private int unreadCount; + private boolean forcedUnread; private int distributionType; private boolean archived; private long expiresIn; @@ -262,6 +276,11 @@ public final class ThreadRecord { return this; } + public Builder setForcedUnread(boolean forcedUnread) { + this.forcedUnread = forcedUnread; + return this; + } + public Builder setDistributionType(int distributionType) { this.distributionType = distributionType; return this; @@ -283,10 +302,12 @@ public final class ThreadRecord { } public ThreadRecord build() { - Preconditions.checkArgument(threadId > 0); - Preconditions.checkArgument(date > 0); - Preconditions.checkNotNull(body); - Preconditions.checkNotNull(recipient); + if (distributionType == ThreadDatabase.DistributionTypes.CONVERSATION) { + Preconditions.checkArgument(threadId > 0); + Preconditions.checkArgument(date > 0); + Preconditions.checkNotNull(body); + Preconditions.checkNotNull(recipient); + } return new ThreadRecord(this); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/InitialMessageRetriever.java b/app/src/main/java/org/thoughtcrime/securesms/messages/InitialMessageRetriever.java index f2e23e616f..2bf01eee35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/InitialMessageRetriever.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/InitialMessageRetriever.java @@ -8,7 +8,6 @@ import androidx.annotation.WorkerThread; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import java.util.List; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/views/DarkOverflowToolbar.java b/app/src/main/java/org/thoughtcrime/securesms/util/views/DarkOverflowToolbar.java new file mode 100644 index 0000000000..e39be0d768 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/views/DarkOverflowToolbar.java @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.util.views; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.util.AttributeSet; + +import androidx.annotation.Nullable; +import androidx.appcompat.widget.Toolbar; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.ThemeUtil; + +/** + * It seems to be impossible to tint the overflow icon in an ActionMode independently from the + * default toolbar overflow icon. So we default the overflow icon to white, then we can use this + * subclass to make it the correct themed color for most use cases. + */ +public class DarkOverflowToolbar extends Toolbar { + public DarkOverflowToolbar(Context context) { + super(context); + init(); + } + + public DarkOverflowToolbar(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public DarkOverflowToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + if (getOverflowIcon() != null) { + getOverflowIcon().setColorFilter(ThemeUtil.getThemedColor(getContext(), R.attr.icon_tint), PorterDuff.Mode.SRC_ATOP); + } + } +} diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index faedaac1d7..e13cb899df 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -7,7 +7,7 @@ android:layout_height="match_parent" android:background="?android:windowBackground"> - - + - + - + - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf1ea31ee9..3fb3c5b105 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1906,6 +1906,9 @@ Select all Archive selected Unarchive selected + Unarchive selected + Mark as read + Mark as unread Settings shortcut diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 4486e28781..3855fea1aa 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -326,7 +326,7 @@