Add support for mark as unread.

master
Greyson Parrelli 2020-05-28 09:27:00 -04:00
parent 6b2e000e61
commit d70c33d20f
13 changed files with 302 additions and 83 deletions

View File

@ -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<Long> typingThreads,
@NonNull Set<Long> selectedThreads, boolean batchMode);
void bind(@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull Set<Long> selectedThreads, boolean batchMode);
}

View File

@ -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;

View File

@ -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<ConversationList
private final @Nullable ItemClickListener clickListener;
private final @NonNull MessageDigest digest;
private final Set<Long> batchSet = Collections.synchronizedSet(new HashSet<Long>());
private boolean batchMode = false;
private final Set<Long> typingSet = new HashSet<>();
private final Map<Long, ThreadRecord> batchSet = Collections.synchronizedMap(new HashMap<>());
private boolean batchMode = false;
private final Set<Long> typingSet = new HashSet<>();
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationListItem> ViewHolder(final @NonNull V itemView)
@ -143,7 +148,7 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
viewHolder.getItem().bind(getThreadRecord(cursor), glideRequests, locale, typingSet, batchSet, batchMode);
viewHolder.getItem().bind(getThreadRecord(cursor), glideRequests, locale, typingSet, batchSet.keySet(), batchMode);
}
@Override
@ -169,16 +174,20 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
return threadDatabase.readerFor(cursor).getCurrent();
}
void toggleThreadInBatchSet(long threadId) {
if (batchSet.contains(threadId)) {
batchSet.remove(threadId);
} else if (threadId != -1) {
batchSet.add(threadId);
void toggleThreadInBatchSet(@NonNull ThreadRecord thread) {
if (batchSet.containsKey(thread.getThreadId())) {
batchSet.remove(thread.getThreadId());
} else if (thread.getThreadId() != -1) {
batchSet.put(thread.getThreadId(), thread);
}
}
Set<Long> getBatchSelections() {
return batchSet;
@NonNull Set<Long> getBatchSelectionIds() {
return batchSet.keySet();
}
@NonNull Set<ThreadRecord> getBatchSelection() {
return new HashSet<>(batchSet.values());
}
void initializeBatchMode(boolean toggle) {
@ -193,8 +202,10 @@ class ConversationListAdapter extends CursorRecyclerViewAdapter<ConversationList
void selectAllThreads() {
for (int i = 0; i < getItemCount(); i++) {
long threadId = getThreadRecord(getCursorAtPositionOrThrow(i)).getThreadId();
if (threadId != -1) batchSet.add(threadId);
ThreadRecord record = getThreadRecord(getCursorAtPositionOrThrow(i));
if (record.getThreadId() != -1) {
batchSet.put(record.getThreadId(), record);
}
}
this.notifyDataSetChanged();
}

View File

@ -29,6 +29,7 @@ import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
@ -65,6 +66,7 @@ import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.Stream;
import com.google.android.material.snackbar.Snackbar;
import org.greenrobot.eventbus.EventBus;
@ -90,6 +92,7 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.conversation.ConversationAdapter;
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
@ -116,10 +119,12 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
@ -593,6 +598,41 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
});
}
private void handleMarkSelectedAsRead() {
Context context = requireContext();
Set<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelectionIds());
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
List<MarkedMessageInfo> 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<Long> 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<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelections());
Set<Long> 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<Long> selectedConversations = defaultAdapter.getBatchSelections();
final Set<Long> selectedConversations = defaultAdapter.getBatchSelectionIds();
if (!selectedConversations.isEmpty()) {
new AsyncTask<Void, Void, Void>() {
@ -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;
}

View File

@ -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);
}

View File

@ -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<MarkedMessageInfo> 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<MarkedMessageInfo> 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<MarkedMessageInfo> setRead(Collection<Long> threadIds, boolean lastSeen) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""});
List<MarkedMessageInfo> smsRecords = new LinkedList<>();
List<MarkedMessageInfo> mmsRecords = new LinkedList<>();
final List<MarkedMessageInfo> smsRecords = DatabaseFactory.getSmsDatabase(context).setMessagesRead(threadId);
final List<MarkedMessageInfo> 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<Long> 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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -7,7 +7,7 @@
android:layout_height="match_parent"
android:background="?android:windowBackground">
<androidx.appcompat.widget.Toolbar
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
@ -66,7 +66,7 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
</org.thoughtcrime.securesms.util.views.DarkOverflowToolbar>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_basic"

View File

@ -1,15 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/conversation_list_batch__menu_delete_selected"
android:id="@+id/menu_delete_selected"
android:icon="?menu_trash_icon"
app:showAsAction="always" />
<item
android:id="@+id/menu_delete_selected"
android:icon="?menu_trash_icon"
android:title="@string/conversation_list_batch__menu_delete_selected"
app:showAsAction="always" />
<item android:title="@string/conversation_list_batch__menu_select_all"
android:id="@+id/menu_select_all"
android:icon="?menu_selectall_icon"
app:showAsAction="always"/>
<item
android:id="@+id/menu_mark_as_read"
android:title="@string/conversation_list_batch__menu_mark_as_read"
app:showAsAction="never" />
<item
android:id="@+id/menu_mark_as_unread"
android:title="@string/conversation_list_batch__menu_mark_as_unread"
app:showAsAction="never" />
<item
android:id="@+id/menu_select_all"
android:title="@string/conversation_list_batch__menu_select_all"
app:showAsAction="never" />
</menu>

View File

@ -1906,6 +1906,9 @@
<string name="conversation_list_batch__menu_select_all">Select all</string>
<string name="conversation_list_batch_archive__menu_archive_selected">Archive selected</string>
<string name="conversation_list_batch_unarchive__menu_unarchive_selected">Unarchive selected</string>
<string name="conversation_list_batch__menu_unarchive_selected">Unarchive selected</string>
<string name="conversation_list_batch__menu_mark_as_read">Mark as read</string>
<string name="conversation_list_batch__menu_mark_as_unread">Mark as unread</string>
<!-- conversation_list -->
<string name="conversation_list_settings_shortcut">Settings shortcut</string>

View File

@ -326,7 +326,7 @@
<style name="Signal.Toolbar.Overflow" parent="Widget.AppCompat.ActionButton.Overflow">
<item name="srcCompat">@drawable/ic_more_vert_24</item>
<item name="android:src">@null</item>
<item name="android:tint">?icon_tint</item>
<item name="android:tint">@color/core_white</item>
</style>
<style name="Signal.Toolbar.Overflow.Conversation">