From 04215f74e1dcf43f8e5c8e174d5bfe3f7d541e08 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 28 May 2024 16:44:49 +0930 Subject: [PATCH] Bind message requests in HomeAdapter --- .../securesms/home/HomeActivity.kt | 34 +--------- .../securesms/home/HomeAdapter.kt | 57 +++++++++++----- .../securesms/home/HomeViewModel.kt | 68 ++++++++++++------- 3 files changed, 86 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 79684d83d5..c063f30538 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -16,7 +16,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect @@ -28,7 +27,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.databinding.ActivityHomeBinding -import network.loki.messenger.databinding.ViewMessageRequestBannerBinding import network.loki.messenger.libsession_util.ConfigBase import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -74,13 +72,11 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities -import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show import java.io.IOException -import java.util.Locale import javax.inject.Inject @AndroidEntryPoint @@ -113,7 +109,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), get() = textSecurePreferences.getLocalNumber()!! private val homeAdapter: HomeAdapter by lazy { - HomeAdapter(context = this, configFactory = configFactory, listener = this) + HomeAdapter(context = this, configFactory = configFactory, listener = this, ::showMessageRequests, ::hideMessageRequests) } private val globalSearchAdapter = GlobalSearchAdapter { model -> @@ -229,7 +225,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } else 0 homeAdapter.data = data if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) } - setupMessageRequestsBanner(data.unapprovedConversationCount, data.hasHiddenMessageRequests) updateEmptyState() } } @@ -340,33 +335,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), binding.newConversationButton.isVisible = !isShown } - private fun setupMessageRequestsBanner(messageRequestCount: Int, hasHiddenMessageRequests: Boolean) { - // Set up message requests - if (messageRequestCount > 0 && !hasHiddenMessageRequests) { - with(ViewMessageRequestBannerBinding.inflate(layoutInflater)) { - unreadCountTextView.text = messageRequestCount.toString() - timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString( - this@HomeActivity, - Locale.getDefault(), - threadDb.latestUnapprovedConversationTimestamp - ) - root.setOnClickListener { showMessageRequests() } - root.setOnLongClickListener { hideMessageRequests(); true } - root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) - val hadHeader = homeAdapter.hasHeaderView() - homeAdapter.header = root - if (hadHeader) homeAdapter.notifyItemChanged(0) - else homeAdapter.notifyItemInserted(0) - } - } else { - val hadHeader = homeAdapter.hasHeaderView() - homeAdapter.header = null - if (hadHeader) { - homeAdapter.notifyItemRemoved(0) - } - } - } - private fun updateLegacyConfigView() { binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset) && textSecurePreferences.getHasLegacyConfig() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index d94037f28c..5203c6ac1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -9,13 +9,18 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_ID import network.loki.messenger.R +import network.loki.messenger.databinding.ViewMessageRequestBannerBinding import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.DateUtils +import java.util.Locale class HomeAdapter( private val context: Context, private val configFactory: ConfigFactory, - private val listener: ConversationClickListener + private val listener: ConversationClickListener, + private val showMessageRequests: () -> Unit, + private val hideMessageRequests: () -> Unit, ) : RecyclerView.Adapter(), ListUpdateCallback { companion object { @@ -23,22 +28,31 @@ class HomeAdapter( private const val ITEM = 1 } - var header: View? = null + var messageRequests: HomeViewModel.MessageRequests? = null + set(value) { + val hadHeader = hasHeaderView() + field = value + if (value != null) { + if (hadHeader) notifyItemChanged(0) else notifyItemInserted(0) + } else if (hadHeader) notifyItemRemoved(0) + } - var data: HomeViewModel.Data = HomeViewModel.Data(emptyList(), 0, false, emptySet()) + var data: HomeViewModel.Data = HomeViewModel.Data() set(newData) { if (field === newData) return + messageRequests = newData.messageRequests + val diff = HomeDiffUtil(field, newData, context, configFactory) val diffResult = DiffUtil.calculateDiff(diff) field = newData diffResult.dispatchUpdatesTo(this as ListUpdateCallback) } - fun hasHeaderView(): Boolean = header != null + fun hasHeaderView(): Boolean = messageRequests != null private val headerCount: Int - get() = if (header == null) 0 else 1 + get() = if (messageRequests == null) 0 else 1 override fun onInserted(position: Int, count: Int) { notifyItemRangeInserted(position + headerCount, count) @@ -67,7 +81,11 @@ class HomeAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = when (viewType) { HEADER -> { - HeaderFooterViewHolder(header!!) + ViewMessageRequestBannerBinding.inflate(LayoutInflater.from(parent.context)).apply { + root.setOnClickListener { showMessageRequests() } + root.setOnLongClickListener { hideMessageRequests(); true } + root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) + }.let(::HeaderFooterViewHolder) } ITEM -> { val conversationView = LayoutInflater.from(parent.context).inflate(R.layout.view_conversation, parent, false) as ConversationView @@ -83,19 +101,27 @@ class HomeAdapter( } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (holder is ConversationViewHolder) { - val offset = if (hasHeaderView()) position - 1 else position - val thread = data.threads[offset] - val isTyping = data.typingThreadIDs.contains(thread.threadId) - holder.view.bind(thread, isTyping, glide) + when (holder) { + is HeaderFooterViewHolder -> { + holder.binding.run { + messageRequests?.let { + unreadCountTextView.text = it.count + timestampTextView.text = it.timestamp + } + } + } + is ConversationViewHolder -> { + val offset = if (hasHeaderView()) position - 1 else position + val thread = data.threads[offset] + val isTyping = data.typingThreadIDs.contains(thread.threadId) + holder.view.bind(thread, isTyping, glide) + } } } override fun onViewRecycled(holder: RecyclerView.ViewHolder) { if (holder is ConversationViewHolder) { holder.view.recycle() - } else { - super.onViewRecycled(holder) } } @@ -107,6 +133,5 @@ class HomeAdapter( class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view) - class HeaderFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) - -} \ No newline at end of file + class HeaderFooterViewHolder(val binding: ViewMessageRequestBannerBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt index 09da7c80a0..32e276939e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt @@ -2,11 +2,13 @@ package org.thoughtcrime.securesms.home import android.content.ContentResolver import android.content.Context +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -20,6 +22,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import org.session.libsession.utilities.TextSecurePreferences @@ -27,7 +30,9 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.observeChanges +import java.util.Locale import javax.inject.Inject import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier @@ -52,13 +57,13 @@ class HomeViewModel @Inject constructor( */ val data: StateFlow = combine( observeConversationList(), - unapprovedConversationCount(), - hasHiddenMessageRequestsFlow(), observeTypingStatus(), + messageRequests(), ::Data - ).stateIn(viewModelScope, SharingStarted.Eagerly, null) + ) + .stateIn(viewModelScope, SharingStarted.Eagerly, null) - private fun hasHiddenMessageRequestsFlow() = TextSecurePreferences.events + private fun hasHiddenMessageRequests() = TextSecurePreferences.events .filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS } .map { prefs.hasHiddenMessageRequests() } .onStart { emit(prefs.hasHiddenMessageRequests()) } @@ -70,40 +75,55 @@ class HomeViewModel @Inject constructor( .onStart { emit(emptySet()) } .distinctUntilChanged() - private fun unapprovedConversationCount() = - contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI) - .flowOn(Dispatchers.IO) - .map { threadDb.unapprovedConversationCount } - .onStart { emit(threadDb.unapprovedConversationCount) } + private fun messageRequests() = combine( + unapprovedConversationCount(), + hasHiddenMessageRequests(), + latestUnapprovedConversationTimestamp(), + ::createMessageRequests + ) + + private fun unapprovedConversationCount() = reloadTriggersAndContentChanges() + .map { threadDb.unapprovedConversationCount } + + private fun latestUnapprovedConversationTimestamp() = reloadTriggersAndContentChanges() + .map { threadDb.latestUnapprovedConversationTimestamp } @Suppress("OPT_IN_USAGE") - private fun observeConversationList(): Flow> = merge( + private fun observeConversationList(): Flow> = reloadTriggersAndContentChanges() + .mapLatest { _ -> + threadDb.approvedConversationList.use { openCursor -> + threadDb.readerFor(openCursor).run { generateSequence { next }.toList() } + } + } + + @OptIn(FlowPreview::class) + private fun reloadTriggersAndContentChanges() = merge( manualReloadTrigger, contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI) ) .flowOn(Dispatchers.IO) .debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS) .onStart { emit(Unit) } - .mapLatest { _ -> - threadDb.approvedConversationList.use { openCursor -> - val reader = threadDb.readerFor(openCursor) - buildList(reader.count) { - while (true) { - add(reader.next ?: break) - } - } - } - } fun tryReload() = manualReloadTrigger.tryEmit(Unit) data class Data( - val threads: List, - val unapprovedConversationCount: Int, - val hasHiddenMessageRequests: Boolean, - val typingThreadIDs: Set + val threads: List = emptyList(), + val typingThreadIDs: Set = emptySet(), + val messageRequests: MessageRequests? = null ) + fun createMessageRequests( + count: Int, + hidden: Boolean, + timestamp: Long + ) = if (count > 0 && !hidden) MessageRequests( + count.toString(), + DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), timestamp) + ) else null + + data class MessageRequests(val count: String, val timestamp: String) + companion object { private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L }