fix: don't set the read flag in update notifications, some roundabout logic for first loads and scrolling to last known positions

This commit is contained in:
0x330a 2023-05-10 12:46:30 +10:00
parent ebdfd2538d
commit cd66901412
6 changed files with 46 additions and 30 deletions

View File

@ -58,6 +58,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
@ -174,6 +175,7 @@ import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale import java.util.Locale
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject import javax.inject.Inject
@ -272,7 +274,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val searchViewModel: SearchViewModel by viewModels() val searchViewModel: SearchViewModel by viewModels()
var searchViewItem: MenuItem? = null var searchViewItem: MenuItem? = null
private val bufferedLastSeenChannel = Channel<Unit>(capacity = Channel.RENDEZVOUS, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val bufferedLastSeenChannel = Channel<Long>(capacity = 512, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val isScrolledToBottom: Boolean private val isScrolledToBottom: Boolean
get() { get() {
@ -340,6 +342,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val cameraButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_camera_24, hasOpaqueBackground = true) } private val cameraButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_camera_24, hasOpaqueBackground = true) }
private val messageToScrollTimestamp = AtomicLong(-1) private val messageToScrollTimestamp = AtomicLong(-1)
private val messageToScrollAuthor = AtomicReference<Address?>(null) private val messageToScrollAuthor = AtomicReference<Address?>(null)
private val firstLoad = AtomicBoolean(true)
private lateinit var reactionDelegate: ConversationReactionDelegate private lateinit var reactionDelegate: ConversationReactionDelegate
private val reactWithAnyEmojiStartPage = -1 private val reactWithAnyEmojiStartPage = -1
@ -440,16 +443,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
// only update the conversation every 3 seconds maximum // only update the conversation every 3 seconds maximum
// channel is rendezvous and shouldn't block on try send calls as often as we want // channel is rendezvous and shouldn't block on try send calls as often as we want
val layoutManager = binding?.conversationRecyclerView?.layoutManager as? LinearLayoutManager ?: return@repeatOnLifecycle val bufferedFlow = bufferedLastSeenChannel.consumeAsFlow()
val lastItemPos = layoutManager.findLastCompletelyVisibleItemPosition() bufferedFlow.filter {
// adapter.item it > storage.getLastSeen(viewModel.threadId)
withContext(Dispatchers.IO) { }.collectLatest { latestMessageRead ->
storage.markConversationAsRead(viewModel.threadId, SnodeAPI.nowWithOffset)
}
val bufferedFlow = bufferedLastSeenChannel.consumeAsFlow().debounce(3.seconds)
bufferedFlow.collectLatest {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
storage.markConversationAsRead(viewModel.threadId, SnodeAPI.nowWithOffset) storage.markConversationAsRead(viewModel.threadId, latestMessageRead)
} }
} }
} }
@ -500,8 +499,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val author = messageToScrollAuthor.getAndSet(null) val author = messageToScrollAuthor.getAndSet(null)
if (author != null && messageTimestamp >= 0) { if (author != null && messageTimestamp >= 0) {
jumpToMessage(author, messageTimestamp, null) jumpToMessage(author, messageTimestamp, null)
} else if (firstLoad.getAndSet(false)) {
scrollToFirstUnreadMessageIfNeeded()
} }
bufferedLastSeenChannel.trySend(Unit)
} }
updatePlaceholder() updatePlaceholder()
} }
@ -990,7 +990,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
binding.typingIndicatorViewContainer.isVisible binding.typingIndicatorViewContainer.isVisible
showOrHideScrollToBottomButton() showOrHideScrollToBottomButton()
val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: -1 val firstVisiblePosition = layoutManager?.findFirstCompletelyVisibleItemPosition() ?: RecyclerView.NO_POSITION
if (!firstLoad.get() && firstVisiblePosition != RecyclerView.NO_POSITION) {
val visibleItemTimestamp = adapter.getTimestampForItemAt(firstVisiblePosition)
if (visibleItemTimestamp != null) {
bufferedLastSeenChannel.trySend(visibleItemTimestamp)
}
}
unreadCount = min(unreadCount, firstVisiblePosition).coerceAtLeast(0) unreadCount = min(unreadCount, firstVisiblePosition).coerceAtLeast(0)
updateUnreadCountIndicator() updateUnreadCountIndicator()
} }

View File

@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.ThreadDatabase.Reader
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
@ -219,11 +220,14 @@ class ConversationAdapter(
fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? { fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? {
val cursor = this.cursor val cursor = this.cursor
if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null if (cursor == null || !isActiveCursor) return null
if (lastSeenTimestamp == 0L && cursor.moveToLast()) {
return cursor.position
}
for (i in 0 until itemCount) { for (i in 0 until itemCount) {
cursor.moveToPosition(i) cursor.moveToPosition(i)
val message = messageDB.readerFor(cursor).current val message = messageDB.readerFor(cursor).current
if (message.isOutgoing || message.dateReceived <= lastSeenTimestamp) { return i } if (message.isOutgoing || message.dateSent <= lastSeenTimestamp) { return i }
} }
return null return null
} }
@ -243,4 +247,11 @@ class ConversationAdapter(
this.searchQuery = query this.searchQuery = query
notifyDataSetChanged() notifyDataSetChanged()
} }
fun getTimestampForItemAt(firstVisiblePosition: Int): Long? {
val cursor = this.cursor ?: return null
if (!cursor.moveToPosition(firstVisiblePosition)) return null
val message = messageDB.readerFor(cursor).current ?: return null
return message.timestamp
}
} }

View File

@ -64,6 +64,7 @@ import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.util.SessionMetaProtocol; import org.thoughtcrime.securesms.util.SessionMetaProtocol;
import java.io.Closeable; import java.io.Closeable;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -313,8 +314,12 @@ public class ThreadDatabase extends Database {
final List<MarkedMessageInfo> smsRecords = DatabaseComponent.get(context).smsDatabase().setMessagesRead(threadId, lastReadTime); final List<MarkedMessageInfo> smsRecords = DatabaseComponent.get(context).smsDatabase().setMessagesRead(threadId, lastReadTime);
final List<MarkedMessageInfo> mmsRecords = DatabaseComponent.get(context).mmsDatabase().setMessagesRead(threadId, lastReadTime); final List<MarkedMessageInfo> mmsRecords = DatabaseComponent.get(context).mmsDatabase().setMessagesRead(threadId, lastReadTime);
ContentValues contentValues = new ContentValues(1); if (smsRecords.isEmpty() && mmsRecords.isEmpty()) {
contentValues.put(READ, 1); return Collections.emptyList();
}
ContentValues contentValues = new ContentValues(2);
contentValues.put(READ, smsRecords.isEmpty() && mmsRecords.isEmpty());
contentValues.put(LAST_SEEN, lastReadTime); contentValues.put(LAST_SEEN, lastReadTime);
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();

View File

@ -240,10 +240,10 @@ public class DefaultMessageNotifier implements MessageNotifier {
!(recipient.isApproved() || threads.getLastSeenAndHasSent(threadId).second())) { !(recipient.isApproved() || threads.getLastSeenAndHasSent(threadId).second())) {
TextSecurePreferences.removeHasHiddenMessageRequests(context); TextSecurePreferences.removeHasHiddenMessageRequests(context);
} }
if (isVisible && recipient != null && threads.getMessageCount(threadId) > 0) { // if (isVisible && recipient != null && threads.getMessageCount(threadId) > 0) {
List<MarkedMessageInfo> messageIds = threads.setRead(threadId, true); // List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
if (SessionMetaProtocol.shouldSendReadReceipt(recipient)) { MarkReadReceiver.process(context, messageIds); } // if (SessionMetaProtocol.shouldSendReadReceipt(recipient)) { MarkReadReceiver.process(context, messageIds); }
} // }
if (!TextSecurePreferences.isNotificationsEnabled(context) || if (!TextSecurePreferences.isNotificationsEnabled(context) ||
(recipient != null && recipient.isMuted())) (recipient != null && recipient.isMuted()))

View File

@ -70,9 +70,8 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
storage.addClosedGroupPublicKey(groupPublicKey) storage.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair // Store the encryption key pair
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTime) storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTime)
// Notify the user // Create the thread
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair) storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair)
// Notify the PN server // Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)

View File

@ -498,13 +498,8 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
storage.setExpirationTimer(groupID, expireTimer) storage.setExpirationTimer(groupID, expireTimer)
// Notify the PN server // Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!) PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!)
// Notify the user // Create thread
if (userPublicKey == sender && !groupExists) { storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp)
} else if (userPublicKey != sender) {
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
}
// Start polling // Start polling
ClosedGroupPollerV2.shared.startPolling(groupPublicKey) ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
} }