Merge branch 'dev' into poller-fix

This commit is contained in:
Ryan ZHAO 2024-05-29 15:35:13 +10:00
commit 0d1ec5ed0b
6 changed files with 146 additions and 120 deletions

View File

@ -40,18 +40,16 @@ object ResendMessageUtilities {
message.recipient = messageRecord.recipient.address.serialize() message.recipient = messageRecord.recipient.address.serialize()
} }
message.threadID = messageRecord.threadId message.threadID = messageRecord.threadId
if (messageRecord.isMms) { if (messageRecord.isMms && messageRecord is MmsMessageRecord) {
val mmsMessageRecord = messageRecord as MmsMessageRecord messageRecord.linkPreviews.firstOrNull()?.let { message.linkPreview = LinkPreview.from(it) }
if (mmsMessageRecord.linkPreviews.isNotEmpty()) { messageRecord.quote?.quoteModel?.let {
message.linkPreview = LinkPreview.from(mmsMessageRecord.linkPreviews[0]) message.quote = Quote.from(it)?.apply {
} if (userBlindedKey != null && publicKey == TextSecurePreferences.getLocalNumber(context)) {
if (mmsMessageRecord.quote != null) { publicKey = userBlindedKey
message.quote = Quote.from(mmsMessageRecord.quote!!.quoteModel) }
if (userBlindedKey != null && messageRecord.quote!!.author.serialize() == TextSecurePreferences.getLocalNumber(context)) {
message.quote!!.publicKey = userBlindedKey
} }
} }
message.addSignalAttachments(mmsMessageRecord.slideDeck.asAttachments()) message.addSignalAttachments(messageRecord.slideDeck.asAttachments())
} }
val sentTimestamp = message.sentTimestamp val sentTimestamp = message.sentTimestamp
val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey() val sender = MessagingModuleConfiguration.shared.storage.getUserPublicKey()

View File

@ -1147,13 +1147,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
} }
} }
fun readerFor(cursor: Cursor?): Reader { fun readerFor(cursor: Cursor?, getQuote: Boolean = true) = Reader(cursor, getQuote)
return Reader(cursor)
}
fun readerFor(message: OutgoingMediaMessage?, threadId: Long): OutgoingMessageReader { fun readerFor(message: OutgoingMediaMessage?, threadId: Long) = OutgoingMessageReader(message, threadId)
return OutgoingMessageReader(message, threadId)
}
fun setQuoteMissing(messageId: Long): Int { fun setQuoteMissing(messageId: Long): Int {
val contentValues = ContentValues() val contentValues = ContentValues()
@ -1217,7 +1213,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
} }
inner class Reader(private val cursor: Cursor?) : Closeable { inner class Reader(private val cursor: Cursor?, private val getQuote: Boolean = true) : Closeable {
val next: MessageRecord? val next: MessageRecord?
get() = if (cursor == null || !cursor.moveToNext()) null else current get() = if (cursor == null || !cursor.moveToNext()) null else current
val current: MessageRecord val current: MessageRecord
@ -1226,7 +1222,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
return if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND.toLong()) { return if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND.toLong()) {
getNotificationMmsMessageRecord(cursor) getNotificationMmsMessageRecord(cursor)
} else { } else {
getMediaMmsMessageRecord(cursor) getMediaMmsMessageRecord(cursor, getQuote)
} }
} }
@ -1253,20 +1249,10 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
DELIVERY_RECEIPT_COUNT DELIVERY_RECEIPT_COUNT
) )
) )
var readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) val readReceiptCount = if (isReadReceiptsEnabled(context)) cursor.getInt(cursor.getColumnIndexOrThrow(READ_RECEIPT_COUNT)) else 0
val subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SUBSCRIPTION_ID))
val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1) val hasMention = (cursor.getInt(cursor.getColumnIndexOrThrow(HAS_MENTION)) == 1)
if (!isReadReceiptsEnabled(context)) { val contentLocationBytes: ByteArray? = contentLocation?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
readReceiptCount = 0 val transactionIdBytes: ByteArray? = transactionId?.takeUnless { it.isEmpty() }?.let(::toIsoBytes)
}
var contentLocationBytes: ByteArray? = null
var transactionIdBytes: ByteArray? = null
if (!contentLocation.isNullOrEmpty()) contentLocationBytes = toIsoBytes(
contentLocation
)
if (!transactionId.isNullOrEmpty()) transactionIdBytes = toIsoBytes(
transactionId
)
val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize)) val slideDeck = SlideDeck(context, MmsNotificationAttachment(status, messageSize))
return NotificationMmsMessageRecord( return NotificationMmsMessageRecord(
id, recipient, recipient, id, recipient, recipient,
@ -1277,7 +1263,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
) )
} }
private fun getMediaMmsMessageRecord(cursor: Cursor): MediaMmsMessageRecord { private fun getMediaMmsMessageRecord(cursor: Cursor, getQuote: Boolean): MediaMmsMessageRecord {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)) val dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT))
val dateReceived = cursor.getLong( val dateReceived = cursor.getLong(
@ -1328,7 +1314,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
.filterNot { o: DatabaseAttachment? -> o in contactAttachments } .filterNot { o: DatabaseAttachment? -> o in contactAttachments }
.filterNot { o: DatabaseAttachment? -> o in previewAttachments } .filterNot { o: DatabaseAttachment? -> o in previewAttachments }
) )
val quote = getQuote(cursor) val quote = if (getQuote) getQuote(cursor) else null
val reactions = get(context).reactionDatabase().getReactions(cursor) val reactions = get(context).reactionDatabase().getReactions(cursor)
return MediaMmsMessageRecord( return MediaMmsMessageRecord(
id, recipient, recipient, id, recipient, recipient,
@ -1381,7 +1367,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)) val quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID))
val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR)) val quoteAuthor = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR))
if (quoteId == 0L || quoteAuthor.isNullOrBlank()) return null if (quoteId == 0L || quoteAuthor.isNullOrBlank()) return null
val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor) val retrievedQuote = get(context).mmsSmsDatabase().getMessageFor(quoteId, quoteAuthor, false)
val quoteText = retrievedQuote?.body val quoteText = retrievedQuote?.body
val quoteMissing = retrievedQuote == null val quoteMissing = retrievedQuote == null
val quoteDeck = ( val quoteDeck = (

View File

@ -97,9 +97,13 @@ public class MmsSmsDatabase extends Database {
} }
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) { public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
return getMessageFor(timestamp, serializedAuthor, true);
}
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor, boolean getQuote) {
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) { try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
MmsSmsDatabase.Reader reader = readerFor(cursor); MmsSmsDatabase.Reader reader = readerFor(cursor, getQuote);
MessageRecord messageRecord; MessageRecord messageRecord;
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor); boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
@ -635,7 +639,11 @@ public class MmsSmsDatabase extends Database {
} }
public Reader readerFor(@NonNull Cursor cursor) { public Reader readerFor(@NonNull Cursor cursor) {
return new Reader(cursor); return readerFor(cursor, true);
}
public Reader readerFor(@NonNull Cursor cursor, boolean getQuote) {
return new Reader(cursor, getQuote);
} }
@NotNull @NotNull
@ -658,11 +666,13 @@ public class MmsSmsDatabase extends Database {
public class Reader implements Closeable { public class Reader implements Closeable {
private final Cursor cursor; private final Cursor cursor;
private final boolean getQuote;
private SmsDatabase.Reader smsReader; private SmsDatabase.Reader smsReader;
private MmsDatabase.Reader mmsReader; private MmsDatabase.Reader mmsReader;
public Reader(Cursor cursor) { public Reader(Cursor cursor, boolean getQuote) {
this.cursor = cursor; this.cursor = cursor;
this.getQuote = getQuote;
} }
private SmsDatabase.Reader getSmsReader() { private SmsDatabase.Reader getSmsReader() {
@ -675,7 +685,7 @@ public class MmsSmsDatabase extends Database {
private MmsDatabase.Reader getMmsReader() { private MmsDatabase.Reader getMmsReader() {
if (mmsReader == null) { if (mmsReader == null) {
mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor); mmsReader = DatabaseComponent.get(context).mmsDatabase().readerFor(cursor, getQuote);
} }
return mmsReader; return mmsReader;

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 ->
@ -185,7 +181,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.seedReminderView.isVisible = false binding.seedReminderView.isVisible = false
} }
} }
setupMessageRequestsBanner()
// Set up recycler view // Set up recycler view
binding.globalSearchInputLayout.listener = this binding.globalSearchInputLayout.listener = this
homeAdapter.setHasStableIds(true) homeAdapter.setHasStableIds(true)
@ -218,9 +213,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Subscribe to threads and update the UI // Subscribe to threads and update the UI
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.threads homeViewModel.data
.filterNotNull() // We don't actually want the null value here as it indicates a loading state (maybe we need a loading state?) .filterNotNull() // We don't actually want the null value here as it indicates a loading state (maybe we need a loading state?)
.collectLatest { threads -> .collectLatest { data ->
val manager = binding.recyclerView.layoutManager as LinearLayoutManager val manager = binding.recyclerView.layoutManager as LinearLayoutManager
val firstPos = manager.findFirstCompletelyVisibleItemPosition() val firstPos = manager.findFirstCompletelyVisibleItemPosition()
val offsetTop = if(firstPos >= 0) { val offsetTop = if(firstPos >= 0) {
@ -228,9 +223,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view) manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
} ?: 0 } ?: 0
} else 0 } else 0
homeAdapter.data = threads homeAdapter.data = data
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) } if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
setupMessageRequestsBanner()
updateEmptyState() updateEmptyState()
} }
} }
@ -341,34 +335,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.newConversationButton.isVisible = !isShown binding.newConversationButton.isVisible = !isShown
} }
private fun setupMessageRequestsBanner() {
val messageRequestCount = threadDb.unapprovedConversationCount
// Set up message requests
if (messageRequestCount > 0 && !textSecurePreferences.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()
@ -664,7 +630,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
text("Hide message requests?") text("Hide message requests?")
button(R.string.yes) { button(R.string.yes) {
textSecurePreferences.setHasHiddenMessageRequests() textSecurePreferences.setHasHiddenMessageRequests()
setupMessageRequestsBanner()
homeViewModel.tryReload() homeViewModel.tryReload()
} }
button(R.string.no) button(R.string.no)

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,13 +28,21 @@ class HomeAdapter(
private const val ITEM = 1 private const val ITEM = 1
} }
var header: View? = null var messageRequests: HomeViewModel.MessageRequests? = null
set(value) {
if (field == value) return
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(), emptySet()) var data: HomeViewModel.Data = HomeViewModel.Data()
set(newData) { set(newData) {
if (field === newData) { if (field === newData) return
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)
@ -37,10 +50,10 @@ class HomeAdapter(
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)
@ -69,7 +82,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
@ -85,19 +102,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)
} }
} }
@ -109,6 +134,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
@ -15,24 +17,31 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
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 kotlinx.coroutines.withContext import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext 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
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor( class HomeViewModel @Inject constructor(
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
@ApplicationContextQualifier private val context: Context, private val prefs: TextSecurePreferences,
@ApplicationContextQualifier private val context: Context,
) : ViewModel() { ) : ViewModel() {
// SharedFlow that emits whenever the user asks us to reload the conversation // SharedFlow that emits whenever the user asks us to reload the conversation
private val manualReloadTrigger = MutableSharedFlow<Unit>( private val manualReloadTrigger = MutableSharedFlow<Unit>(
@ -46,8 +55,19 @@ class HomeViewModel @Inject constructor(
* This flow will emit whenever the user asks us to reload the conversation list or * This flow will emit whenever the user asks us to reload the conversation list or
* whenever the conversation list changes. * whenever the conversation list changes.
*/ */
val threads: StateFlow<Data?> = combine(observeConversationList(), observeTypingStatus(), ::Data) val data: StateFlow<Data?> = combine(
.stateIn(viewModelScope, SharingStarted.Eagerly, null) observeConversationList(),
observeTypingStatus(),
messageRequests(),
::Data
)
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private fun hasHiddenMessageRequests() = TextSecurePreferences.events
.filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS }
.flowOn(Dispatchers.IO)
.map { prefs.hasHiddenMessageRequests() }
.onStart { emit(prefs.hasHiddenMessageRequests()) }
private fun observeTypingStatus(): Flow<Set<Long>> = private fun observeTypingStatus(): Flow<Set<Long>> =
ApplicationContext.getInstance(context).typingStatusRepository ApplicationContext.getInstance(context).typingStatusRepository
@ -56,32 +76,55 @@ class HomeViewModel @Inject constructor(
.onStart { emit(emptySet()) } .onStart { emit(emptySet()) }
.distinctUntilChanged() .distinctUntilChanged()
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") @Suppress("OPT_IN_USAGE")
private fun observeConversationList(): Flow<List<ThreadRecord>> = merge( private fun observeConversationList(): Flow<List<ThreadRecord>> = reloadTriggersAndContentChanges()
manualReloadTrigger, .mapLatest { _ ->
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI)) threadDb.approvedConversationList.use { openCursor ->
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS) threadDb.readerFor(openCursor).run { generateSequence { next }.toList() }
.onStart { emit(Unit) }
.mapLatest { _ ->
withContext(Dispatchers.IO) {
threadDb.approvedConversationList.use { openCursor ->
val reader = threadDb.readerFor(openCursor)
buildList(reader.count) {
while (true) {
add(reader.next ?: break)
}
}
}
}
} }
}
@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) }
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 typingThreadIDs: Set<Long> val typingThreadIDs: Set<Long> = 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 { companion object {
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
} }