Bind message requests in HomeAdapter

This commit is contained in:
Andrew 2024-05-28 16:44:49 +09:30
parent cd302f9f27
commit 04215f74e1
3 changed files with 86 additions and 73 deletions

View File

@ -16,7 +16,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -28,7 +27,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityHomeBinding import network.loki.messenger.databinding.ActivityHomeBinding
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.ConfigBase
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@ -74,13 +72,11 @@ import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showMuteDialog
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import java.io.IOException import java.io.IOException
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -113,7 +109,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
get() = textSecurePreferences.getLocalNumber()!! get() = textSecurePreferences.getLocalNumber()!!
private val homeAdapter: HomeAdapter by lazy { 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 -> private val globalSearchAdapter = GlobalSearchAdapter { model ->
@ -229,7 +225,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} else 0 } else 0
homeAdapter.data = data homeAdapter.data = data
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) } if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
setupMessageRequestsBanner(data.unapprovedConversationCount, data.hasHiddenMessageRequests)
updateEmptyState() updateEmptyState()
} }
} }
@ -340,33 +335,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.newConversationButton.isVisible = !isShown 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() { private fun updateLegacyConfigView() {
binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset) binding.configOutdatedView.isVisible = ConfigBase.isNewConfigEnabled(textSecurePreferences.hasForcedNewConfig(), SnodeAPI.nowWithOffset)
&& textSecurePreferences.getHasLegacyConfig() && textSecurePreferences.getHasLegacyConfig()

View File

@ -9,13 +9,18 @@ import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.NO_ID import androidx.recyclerview.widget.RecyclerView.NO_ID
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewMessageRequestBannerBinding
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
class HomeAdapter( class HomeAdapter(
private val context: Context, private val context: Context,
private val configFactory: ConfigFactory, private val configFactory: ConfigFactory,
private val listener: ConversationClickListener private val listener: ConversationClickListener,
private val showMessageRequests: () -> Unit,
private val hideMessageRequests: () -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), ListUpdateCallback { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), ListUpdateCallback {
companion object { companion object {
@ -23,22 +28,31 @@ class HomeAdapter(
private const val ITEM = 1 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) { set(newData) {
if (field === newData) return if (field === newData) return
messageRequests = newData.messageRequests
val diff = HomeDiffUtil(field, newData, context, configFactory) val diff = HomeDiffUtil(field, newData, context, configFactory)
val diffResult = DiffUtil.calculateDiff(diff) val diffResult = DiffUtil.calculateDiff(diff)
field = newData field = newData
diffResult.dispatchUpdatesTo(this as ListUpdateCallback) diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
} }
fun hasHeaderView(): Boolean = header != null fun hasHeaderView(): Boolean = messageRequests != null
private val headerCount: Int 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) { override fun onInserted(position: Int, count: Int) {
notifyItemRangeInserted(position + headerCount, count) notifyItemRangeInserted(position + headerCount, count)
@ -67,7 +81,11 @@ class HomeAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
when (viewType) { when (viewType) {
HEADER -> { 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 -> { ITEM -> {
val conversationView = LayoutInflater.from(parent.context).inflate(R.layout.view_conversation, parent, false) as ConversationView 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) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ConversationViewHolder) { 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 offset = if (hasHeaderView()) position - 1 else position
val thread = data.threads[offset] val thread = data.threads[offset]
val isTyping = data.typingThreadIDs.contains(thread.threadId) val isTyping = data.typingThreadIDs.contains(thread.threadId)
holder.view.bind(thread, isTyping, glide) holder.view.bind(thread, isTyping, glide)
} }
} }
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) { override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is ConversationViewHolder) { if (holder is ConversationViewHolder) {
holder.view.recycle() holder.view.recycle()
} else {
super.onViewRecycled(holder)
} }
} }
@ -107,6 +133,5 @@ class HomeAdapter(
class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view) class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
class HeaderFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) class HeaderFooterViewHolder(val binding: ViewMessageRequestBannerBinding) : RecyclerView.ViewHolder(binding.root)
} }

View File

@ -2,11 +2,13 @@ package org.thoughtcrime.securesms.home
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -20,6 +22,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import org.session.libsession.utilities.TextSecurePreferences 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.DatabaseContentProviders
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.observeChanges import org.thoughtcrime.securesms.util.observeChanges
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier
@ -52,13 +57,13 @@ class HomeViewModel @Inject constructor(
*/ */
val data: StateFlow<Data?> = combine( val data: StateFlow<Data?> = combine(
observeConversationList(), observeConversationList(),
unapprovedConversationCount(),
hasHiddenMessageRequestsFlow(),
observeTypingStatus(), observeTypingStatus(),
messageRequests(),
::Data ::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 } .filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS }
.map { prefs.hasHiddenMessageRequests() } .map { prefs.hasHiddenMessageRequests() }
.onStart { emit(prefs.hasHiddenMessageRequests()) } .onStart { emit(prefs.hasHiddenMessageRequests()) }
@ -70,40 +75,55 @@ class HomeViewModel @Inject constructor(
.onStart { emit(emptySet()) } .onStart { emit(emptySet()) }
.distinctUntilChanged() .distinctUntilChanged()
private fun unapprovedConversationCount() = private fun messageRequests() = combine(
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI) unapprovedConversationCount(),
.flowOn(Dispatchers.IO) hasHiddenMessageRequests(),
latestUnapprovedConversationTimestamp(),
::createMessageRequests
)
private fun unapprovedConversationCount() = reloadTriggersAndContentChanges()
.map { threadDb.unapprovedConversationCount } .map { threadDb.unapprovedConversationCount }
.onStart { emit(threadDb.unapprovedConversationCount) }
private fun latestUnapprovedConversationTimestamp() = reloadTriggersAndContentChanges()
.map { threadDb.latestUnapprovedConversationTimestamp }
@Suppress("OPT_IN_USAGE") @Suppress("OPT_IN_USAGE")
private fun observeConversationList(): Flow<List<ThreadRecord>> = merge( private fun observeConversationList(): Flow<List<ThreadRecord>> = reloadTriggersAndContentChanges()
.mapLatest { _ ->
threadDb.approvedConversationList.use { openCursor ->
threadDb.readerFor(openCursor).run { generateSequence { next }.toList() }
}
}
@OptIn(FlowPreview::class)
private fun reloadTriggersAndContentChanges() = merge(
manualReloadTrigger, manualReloadTrigger,
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI) contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI)
) )
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS) .debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
.onStart { emit(Unit) } .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) fun tryReload() = manualReloadTrigger.tryEmit(Unit)
data class Data( data class Data(
val threads: List<ThreadRecord>, val threads: List<ThreadRecord> = emptyList(),
val unapprovedConversationCount: Int, val typingThreadIDs: Set<Long> = emptySet(),
val hasHiddenMessageRequests: Boolean, val messageRequests: MessageRequests? = null
val typingThreadIDs: Set<Long>
) )
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 { companion object {
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
} }