Add support for mark as unread.
parent
6b2e000e61
commit
d70c33d20f
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue