|
|
@ -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
|
|
|
|
}
|
|
|
|
}
|
|
|
|