mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-25 01:07:47 +00:00
Bind message requests in HomeAdapter
This commit is contained in:
parent
cd302f9f27
commit
04215f74e1
@ -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()
|
||||||
|
@ -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) {
|
||||||
val offset = if (hasHeaderView()) position - 1 else position
|
is HeaderFooterViewHolder -> {
|
||||||
val thread = data.threads[offset]
|
holder.binding.run {
|
||||||
val isTyping = data.typingThreadIDs.contains(thread.threadId)
|
messageRequests?.let {
|
||||||
holder.view.bind(thread, isTyping, glide)
|
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) {
|
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)
|
||||||
|
|
||||||
}
|
}
|
@ -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(),
|
||||||
.map { threadDb.unapprovedConversationCount }
|
latestUnapprovedConversationTimestamp(),
|
||||||
.onStart { emit(threadDb.unapprovedConversationCount) }
|
::createMessageRequests
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun unapprovedConversationCount() = reloadTriggersAndContentChanges()
|
||||||
|
.map { 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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user