change compose sendintent (sending sms/mms) and sendmessage interactor to ensure convos congruency

change compose selectedconversation (chips -> convo monitoring) to take advantage of changes in conversationrepo
conversationrepo change getthreadid to getconversation to avoid duplicate db access
some cleanups and refactoring in conversationrepo
This commit is contained in:
gavine99
2025-04-07 14:52:04 +10:00
parent 27a836324e
commit 821b02ad58
6 changed files with 402 additions and 471 deletions
@@ -20,8 +20,6 @@ package dev.octoshrimpy.quik.repository
import android.content.ContentUris
import android.content.Context
import android.provider.Telephony
import android.view.View.OnLongClickListener
import dev.octoshrimpy.quik.compat.TelephonyCompat
import dev.octoshrimpy.quik.extensions.anyOf
import dev.octoshrimpy.quik.extensions.asObservable
@@ -66,21 +64,21 @@ class ConversationRepositoryImpl @Inject constructor(
}
return Realm.getDefaultInstance()
.where(Conversation::class.java)
.notEqualTo("id", 0L)
.equalTo("archived", archived)
.equalTo("blocked", false)
.isNotEmpty("recipients")
.beginGroup()
.isNotNull("lastMessage")
.or()
.isNotEmpty("draft")
.endGroup()
.sort(
sortOrder.toTypedArray(),
sortDirections.toTypedArray()
)
.findAllAsync()
.where(Conversation::class.java)
.notEqualTo("id", 0L)
.equalTo("archived", archived)
.equalTo("blocked", false)
.isNotEmpty("recipients")
.beginGroup()
.isNotNull("lastMessage")
.or()
.isNotEmpty("draft")
.endGroup()
.sort(
sortOrder.toTypedArray(),
sortDirections.toTypedArray()
)
.findAllAsync()
}
override fun getConversationsSnapshot(unreadAtTop: Boolean): List<Conversation> {
@@ -94,60 +92,49 @@ class ConversationRepositoryImpl @Inject constructor(
return Realm.getDefaultInstance().use { realm ->
realm.refresh()
realm.copyFromRealm(realm.where(Conversation::class.java)
.notEqualTo("id", 0L)
.equalTo("archived", false)
.equalTo("blocked", false)
.isNotEmpty("recipients")
.beginGroup()
.isNotNull("lastMessage")
.or()
.isNotEmpty("draft")
.endGroup()
.sort(
sortOrder.toTypedArray(),
sortDirections.toTypedArray()
)
.findAll())
realm.where(Conversation::class.java)
.notEqualTo("id", 0L)
.equalTo("archived", false)
.equalTo("blocked", false)
.isNotEmpty("recipients")
.beginGroup()
.isNotNull("lastMessage")
.or()
.isNotEmpty("draft")
.endGroup()
.sort(sortOrder.toTypedArray(), sortDirections.toTypedArray())
.findAll()
.let(realm::copyFromRealm)
}
}
override fun getTopConversations(): List<Conversation> {
return Realm.getDefaultInstance().use { realm ->
realm.copyFromRealm(realm.where(Conversation::class.java)
.notEqualTo("id", 0L)
.isNotNull("lastMessage")
.beginGroup()
.equalTo("pinned", true)
.or()
.greaterThan("lastMessage.date", System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7))
.endGroup()
.equalTo("archived", false)
.equalTo("blocked", false)
.isNotEmpty("recipients")
.findAll())
.sortedWith(compareByDescending<Conversation> { conversation -> conversation.pinned }
.thenByDescending { conversation ->
realm.where(Message::class.java)
.equalTo("threadId", conversation.id)
.greaterThan("date", System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7))
.count()
})
override fun getTopConversations() =
Realm.getDefaultInstance().use { realm ->
realm.where(Conversation::class.java)
.notEqualTo("id", 0L)
.isNotNull("lastMessage")
.beginGroup()
.equalTo("pinned", true)
.or()
.greaterThan("lastMessage.date", System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7))
.endGroup()
.equalTo("archived", false)
.equalTo("blocked", false)
.isNotEmpty("recipients")
.findAll()
.let(realm::copyFromRealm)
.sortedWith(compareByDescending<Conversation> { conversation -> conversation.pinned }
.thenByDescending { conversation ->
realm.where(Message::class.java)
.equalTo("threadId", conversation.id)
.greaterThan("date", System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7))
.count()
}
)
}
}
// override fun setConversationName(id: Long, name: String) {
// Realm.getDefaultInstance().use { realm ->
// realm.executeTransaction {
// realm.where(Conversation::class.java)
// .equalTo("id", id)
// .findFirst()
// ?.name = name
// }
// }
// }
override fun setConversationName(id: Long, name: String): Completable {
return Completable.fromAction {
override fun setConversationName(id: Long, name: String) =
Completable.fromAction {
Realm.getDefaultInstance().use { realm ->
realm.executeTransaction {
realm.where(Conversation::class.java)
@@ -157,150 +144,134 @@ class ConversationRepositoryImpl @Inject constructor(
}
}
}.subscribeOn(Schedulers.io()) // Ensure the operation is performed on a background thread
}
override fun searchConversations(query: CharSequence): List<SearchResult> {
val realm = Realm.getDefaultInstance()
val normalizedQuery = query.removeAccents()
val conversations = realm.copyFromRealm(realm
.where(Conversation::class.java)
.notEqualTo("id", 0L)
.isNotNull("lastMessage")
.equalTo("blocked", false)
.isNotEmpty("recipients")
.sort("pinned", Sort.DESCENDING, "lastMessage.date", Sort.DESCENDING)
.findAll())
.where(Conversation::class.java)
.notEqualTo("id", 0L)
.isNotNull("lastMessage")
.equalTo("blocked", false)
.isNotEmpty("recipients")
.sort("pinned", Sort.DESCENDING, "lastMessage.date", Sort.DESCENDING)
.findAll())
val messagesByConversation = realm.copyFromRealm(realm
.where(Message::class.java)
.beginGroup()
.contains("body", normalizedQuery, Case.INSENSITIVE)
.or()
.contains("parts.text", normalizedQuery, Case.INSENSITIVE)
.endGroup()
.findAll())
.asSequence()
.groupBy { message -> message.threadId }
.filter { (threadId, _) -> conversations.firstOrNull { it.id == threadId } != null }
.map { (threadId, messages) -> Pair(conversations.first { it.id == threadId }, messages.size) }
.map { (conversation, messages) -> SearchResult(normalizedQuery, conversation, messages) }
.sortedByDescending { result -> result.messages }
.toList()
.where(Message::class.java)
.beginGroup()
.contains("body", normalizedQuery, Case.INSENSITIVE)
.or()
.contains("parts.text", normalizedQuery, Case.INSENSITIVE)
.endGroup()
.findAll())
.asSequence()
.groupBy { message -> message.threadId }
.filter { (threadId, _) -> conversations.firstOrNull { it.id == threadId } != null }
.map { (threadId, messages) -> Pair(conversations.first { it.id == threadId }, messages.size) }
.map { (conversation, messages) -> SearchResult(normalizedQuery, conversation, messages) }
.sortedByDescending { result -> result.messages }
.toList()
realm.close()
return conversations
.filter { conversation -> conversationFilter.filter(conversation, normalizedQuery) }
.map { conversation -> SearchResult(normalizedQuery, conversation, 0) } + messagesByConversation
.filter { conversation -> conversationFilter.filter(conversation, normalizedQuery) }
.map {
conversation -> SearchResult(normalizedQuery, conversation, 0)
} + messagesByConversation
}
override fun getBlockedConversations(): RealmResults<Conversation> {
return Realm.getDefaultInstance()
.where(Conversation::class.java)
.equalTo("blocked", true)
.sort(
arrayOf("lastMessage.date"),
arrayOf(Sort.DESCENDING)
)
.findAll()
}
override fun getBlockedConversations(): RealmResults<Conversation> =
Realm.getDefaultInstance()
.where(Conversation::class.java)
.equalTo("blocked", true)
.sort(
arrayOf("lastMessage.date"),
arrayOf(Sort.DESCENDING)
)
.findAll()
override fun getBlockedConversationsAsync(): RealmResults<Conversation> {
return Realm.getDefaultInstance()
override fun getBlockedConversationsAsync(): RealmResults<Conversation> =
Realm.getDefaultInstance()
.where(Conversation::class.java)
.equalTo("blocked", true)
.sort(
arrayOf("lastMessage.date"),
arrayOf(Sort.DESCENDING)
)
.findAllAsync()
override fun getConversationAsync(threadId: Long): Conversation =
Realm.getDefaultInstance()
.where(Conversation::class.java)
.equalTo("id", threadId)
.findFirstAsync()
override fun getConversation(threadId: Long) =
Realm.getDefaultInstance()
.apply { refresh() }
.where(Conversation::class.java)
.equalTo("id", threadId)
.findFirst()
override fun getUnseenIds(archived: Boolean) =
ArrayList<Long>().apply {
Realm.getDefaultInstance()
.where(Conversation::class.java)
.equalTo("blocked", true)
.notEqualTo("id", 0L)
.equalTo("archived", archived)
.equalTo("blocked", false)
.equalTo("lastMessage.seen", false)
.sort(
arrayOf("lastMessage.date"),
arrayOf(Sort.DESCENDING)
)
.findAllAsync()
}
override fun getConversationAsync(threadId: Long): Conversation {
return Realm.getDefaultInstance()
.where(Conversation::class.java)
.equalTo("id", threadId)
.findFirstAsync()
}
override fun getConversation(threadId: Long): Conversation? {
return Realm.getDefaultInstance()
.apply { refresh() }
.where(Conversation::class.java)
.equalTo("id", threadId)
.findFirst()
}
override fun getUnseenIds(archived: Boolean): List<Long> {
val conversationIds = ArrayList<Long>()
Realm.getDefaultInstance()
.where(Conversation::class.java)
.notEqualTo("id", 0L)
.equalTo("archived", archived)
.equalTo("blocked", false)
.equalTo("lastMessage.seen", false)
.sort(
arrayOf("lastMessage.date"),
arrayOf(Sort.DESCENDING)
)
.findAllAsync()
.forEach { conversation -> conversationIds.add(conversation.id) }
return conversationIds
}
override fun getUnreadIds(archived: Boolean): List<Long> {
val conversationIds = ArrayList<Long>()
Realm.getDefaultInstance()
.where(Conversation::class.java)
.notEqualTo("id", 0L)
.equalTo("archived", archived)
.equalTo("blocked", false)
.equalTo("lastMessage.read", false)
.sort(
arrayOf("lastMessage.date"),
arrayOf(Sort.DESCENDING)
)
.findAllAsync()
.forEach { conversation -> conversationIds.add(conversation.id) }
return conversationIds
}
override fun getConversationAndLastSenderContactName(threadId: Long): Pair<Conversation?, String?>? {
val conversation = Realm.getDefaultInstance()
.apply { refresh() }
.where(Conversation::class.java)
.equalTo("id", threadId)
.findFirst()
if (conversation === null)
return null
var conversationLastSmsSender: String? = null
if (conversation !== null) {
conversationLastSmsSender = conversation?.recipients?.find { recipient ->
phoneNumberUtils.compare(recipient.address, conversation.lastMessage!!.address)
}?.contact?.name
.forEach { conversation -> add(conversation.id) }
}
return return Pair(conversation, conversationLastSmsSender)
}
override fun getConversations(vararg threadIds: Long): RealmResults<Conversation> {
return Realm.getDefaultInstance()
override fun getUnreadIds(archived: Boolean) =
ArrayList<Long>().apply {
Realm.getDefaultInstance()
.where(Conversation::class.java)
.anyOf("id", threadIds)
.findAll()
}
.notEqualTo("id", 0L)
.equalTo("archived", archived)
.equalTo("blocked", false)
.equalTo("lastMessage.read", false)
.sort(
arrayOf("lastMessage.date"),
arrayOf(Sort.DESCENDING)
)
.findAllAsync()
.forEach { conversation -> add(conversation.id) }
}
override fun getUnmanagedConversations(): Observable<List<Conversation>> {
val realm = Realm.getDefaultInstance()
return realm.where(Conversation::class.java)
override fun getConversationAndLastSenderContactName(threadId: Long): Pair<Conversation?, String?>? =
Realm.getDefaultInstance()
.apply { refresh() }
.where(Conversation::class.java)
.equalTo("id", threadId)
.findFirst()
?.let { conversation ->
val conversationLastSmsSender: String? = conversation.recipients.find { recipient ->
phoneNumberUtils.compare(recipient.address, conversation.lastMessage!!.address)
}?.contact?.name
Pair(conversation, conversationLastSmsSender)
}
override fun getConversations(vararg threadIds: Long): RealmResults<Conversation> =
Realm.getDefaultInstance()
.where(Conversation::class.java)
.anyOf("id", threadIds)
.findAll()
override fun getUnmanagedConversations(): Observable<List<Conversation>> =
Realm.getDefaultInstance().let { realm->
realm.where(Conversation::class.java)
.sort("lastMessage.date", Sort.DESCENDING)
.notEqualTo("id", 0L)
.isNotNull("lastMessage")
@@ -315,164 +286,138 @@ class ConversationRepositoryImpl @Inject constructor(
.map { realm.copyFromRealm(it) }
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
}
}
override fun getRecipients(): RealmResults<Recipient> {
val realm = Realm.getDefaultInstance()
return realm.where(Recipient::class.java)
override fun getRecipients(): RealmResults<Recipient> =
Realm.getDefaultInstance()
.where(Recipient::class.java)
.findAll()
}
override fun getUnmanagedRecipients(): Observable<List<Recipient>> {
val realm = Realm.getDefaultInstance()
return realm.where(Recipient::class.java)
override fun getUnmanagedRecipients(): Observable<List<Recipient>> =
Realm.getDefaultInstance().let { realm ->
realm.where(Recipient::class.java)
.isNotNull("contact")
.findAllAsync()
.asObservable()
.filter { it.isLoaded && it.isValid }
.map { realm.copyFromRealm(it) }
.subscribeOn(AndroidSchedulers.mainThread())
}
}
override fun getRecipient(recipientId: Long): Recipient? {
return Realm.getDefaultInstance()
.where(Recipient::class.java)
.equalTo("id", recipientId)
.findFirst()
}
override fun getRecipient(recipientId: Long): Recipient? =
Realm.getDefaultInstance()
.where(Recipient::class.java)
.equalTo("id", recipientId)
.findFirst()
override fun getThreadId(recipient: String): Long? {
return getThreadId(listOf(recipient))
}
override fun getConversation(recipient: String) =
getConversation(listOf(recipient))
override fun getThreadId(recipients: Collection<String>): Long? {
return Realm.getDefaultInstance().use { realm ->
override fun getConversation(recipients: Collection<String>) =
Realm.getDefaultInstance().use { realm ->
realm.refresh()
realm.where(Conversation::class.java)
.findAll()
.asSequence()
.filter { conversation -> conversation.recipients.size == recipients.size }
.find { conversation ->
conversation.recipients.map { it.address }.all { address ->
recipients.any { recipient -> phoneNumberUtils.compare(recipient, address) }
}
}?.id
}
}
override fun getOrCreateConversation(threadId: Long): Conversation? {
return getConversation(threadId) ?: getConversationFromCp(threadId)
}
override fun getOrCreateConversation(address: String): Conversation? {
return getOrCreateConversation(listOf(address))
}
override fun getOrCreateConversation(addresses: List<String>): Conversation? {
if (addresses.isEmpty()) {
return null
}
return (getThreadId(addresses) ?: tryOrNull { TelephonyCompat.getOrCreateThreadId(context, addresses.toSet()) })
?.takeIf { threadId -> threadId != 0L }
?.let { threadId ->
getConversation(threadId)
?.let(Realm.getDefaultInstance()::copyFromRealm)
?: getConversationFromCp(threadId)
.findAll()
.asSequence()
.filter { conversation -> conversation.recipients.size == recipients.size }
.find { conversation ->
conversation.recipients.map { it.address }.all { address ->
recipients.any { recipient -> phoneNumberUtils.compare(recipient, address) }
}
}
}
}
override fun saveDraft(threadId: Long, draft: String) {
override fun getOrCreateConversation(threadId: Long) =
tryOrNull(true) {
getConversation(threadId) ?: createConversationFromCp(threadId)
}
override fun getOrCreateConversation(address: String) =
getOrCreateConversation(listOf(address))
override fun getOrCreateConversation(addresses: Collection<String>) =
tryOrNull(true) {
getConversation(addresses)
?: tryOrNull { TelephonyCompat.getOrCreateThreadId(context, addresses.toSet()) }
?.takeIf { it != 0L }
?.let { threadId -> getOrCreateConversation(threadId) }
}
override fun saveDraft(threadId: Long, draft: String) =
Realm.getDefaultInstance().use { realm ->
realm.refresh()
val conversation = realm.where(Conversation::class.java)
.equalTo("id", threadId)
.findFirst()
.equalTo("id", threadId)
.findFirst()
realm.executeTransaction {
conversation?.takeIf { it.isValid }?.draft = draft
}
}
}
override fun updateConversations(vararg threadIds: Long) {
override fun updateConversations(vararg threadIds: Long) =
Realm.getDefaultInstance().use { realm ->
realm.refresh()
threadIds.forEach { threadId ->
val conversation = realm
.where(Conversation::class.java)
.equalTo("id", threadId)
.findFirst() ?: return
.where(Conversation::class.java)
.equalTo("id", threadId)
.findFirst() ?: return@forEach
val message = realm
.where(Message::class.java)
.equalTo("threadId", threadId)
.sort("date", Sort.DESCENDING)
.findFirst()
.where(Message::class.java)
.equalTo("threadId", threadId)
.sort("date", Sort.DESCENDING)
.findFirst()
realm.executeTransaction {
conversation.lastMessage = message
}
realm.executeTransaction { conversation.lastMessage = message }
}
}
}
override fun markArchived(vararg threadIds: Long) {
override fun markArchived(vararg threadIds: Long) =
Realm.getDefaultInstance().use { realm ->
val conversations = realm.where(Conversation::class.java)
.anyOf("id", threadIds)
.findAll()
.anyOf("id", threadIds)
.findAll()
realm.executeTransaction {
conversations.forEach { it.archived = true }
}
realm.executeTransaction { conversations.forEach { it.archived = true } }
}
}
override fun markUnarchived(vararg threadIds: Long) {
override fun markUnarchived(vararg threadIds: Long) =
Realm.getDefaultInstance().use { realm ->
val conversations = realm.where(Conversation::class.java)
.anyOf("id", threadIds)
.findAll()
.anyOf("id", threadIds)
.findAll()
realm.executeTransaction {
conversations.forEach { it.archived = false }
}
realm.executeTransaction { conversations.forEach { it.archived = false } }
}
}
override fun markPinned(vararg threadIds: Long) {
override fun markPinned(vararg threadIds: Long) =
Realm.getDefaultInstance().use { realm ->
val conversations = realm.where(Conversation::class.java)
.anyOf("id", threadIds)
.findAll()
.anyOf("id", threadIds)
.findAll()
realm.executeTransaction {
conversations.forEach { it.pinned = true }
}
realm.executeTransaction { conversations.forEach { it.pinned = true } }
}
}
override fun markUnpinned(vararg threadIds: Long) {
override fun markUnpinned(vararg threadIds: Long) =
Realm.getDefaultInstance().use { realm ->
val conversations = realm.where(Conversation::class.java)
.anyOf("id", threadIds)
.findAll()
.anyOf("id", threadIds)
.findAll()
realm.executeTransaction {
conversations.forEach { it.pinned = false }
}
realm.executeTransaction { conversations.forEach { it.pinned = false } }
}
}
override fun markBlocked(threadIds: List<Long>, blockingClient: Int, blockReason: String?) {
override fun markBlocked(threadIds: Collection<Long>, blockingClient: Int, blockReason: String?) =
Realm.getDefaultInstance().use { realm ->
val conversations = realm.where(Conversation::class.java)
.anyOf("id", threadIds.toLongArray())
.equalTo("blocked", false)
.findAll()
.anyOf("id", threadIds.toLongArray())
.equalTo("blocked", false)
.findAll()
realm.executeTransaction {
conversations.forEach { conversation ->
@@ -482,13 +427,12 @@ class ConversationRepositoryImpl @Inject constructor(
}
}
}
}
override fun markUnblocked(vararg threadIds: Long) {
override fun markUnblocked(vararg threadIds: Long) =
Realm.getDefaultInstance().use { realm ->
val conversations = realm.where(Conversation::class.java)
.anyOf("id", threadIds)
.findAll()
.anyOf("id", threadIds)
.findAll()
realm.executeTransaction {
conversations.forEach { conversation ->
@@ -498,12 +442,15 @@ class ConversationRepositoryImpl @Inject constructor(
}
}
}
}
override fun deleteConversations(vararg threadIds: Long) {
Realm.getDefaultInstance().use { realm ->
val conversation = realm.where(Conversation::class.java).anyOf("id", threadIds).findAll()
val messages = realm.where(Message::class.java).anyOf("threadId", threadIds).findAll()
val conversation = realm.where(Conversation::class.java)
.anyOf("id", threadIds)
.findAll()
val messages = realm.where(Message::class.java)
.anyOf("threadId", threadIds)
.findAll()
realm.executeTransaction {
conversation.deleteAllFromRealm()
@@ -511,9 +458,12 @@ class ConversationRepositoryImpl @Inject constructor(
}
}
threadIds.forEach { threadId ->
val uri = ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId)
context.contentResolver.delete(uri, null, null)
threadIds.forEach {
context.contentResolver.delete(
ContentUris.withAppendedId(TelephonyCompat.THREADS_CONTENT_URI, it),
null,
null
)
}
}
@@ -524,40 +474,45 @@ class ConversationRepositoryImpl @Inject constructor(
* we can return a [Conversation]. On some devices, the ContentProvider won't return the
* conversation unless it contains at least 1 message
*/
private fun getConversationFromCp(threadId: Long): Conversation? {
return cursorToConversation.getConversationsCursor()
private fun createConversationFromCp(threadId: Long) =
tryOrNull(true) {
cursorToConversation.getConversationsCursor()
?.map(cursorToConversation::map)
?.firstOrNull { it.id == threadId }
?.let { conversation ->
val realm = Realm.getDefaultInstance()
val contacts = realm.copyFromRealm(realm.where(Contact::class.java).findAll())
val lastMessage = realm.where(Message::class.java).equalTo("threadId", threadId)
.sort("date", Sort.DESCENDING).findFirst()?.let(realm::copyFromRealm)
?.firstOrNull { conversation -> conversation.id == threadId }
?.also { conversation ->
Realm.getDefaultInstance().use { realm ->
val realmContacts = realm.where(Contact::class.java).findAll()
val recipients = conversation.recipients
.map { recipient -> recipient.id }
.map { id -> cursorToRecipient.getRecipientCursor(id) }
.mapNotNull { recipientCursor ->
// Map the recipient cursor to a list of recipients
recipientCursor?.use { recipientCursor.map { cursorToRecipient.map(recipientCursor) } }
// match recipients from provider to recipients in realm
val matchedRecipients = conversation.recipients
.mapNotNull { recipient ->
// map the recipient cursor to a list of recipients
cursorToRecipient.getRecipientCursor(recipient.id)?.use { cursor ->
cursor.map { cursorToRecipient.map(it) }
}
}
.flatten()
.map { recipient ->
recipient.apply {
contact = contacts.firstOrNull {
it.numbers.any { phoneNumberUtils.compare(recipient.address, it.address) }
contact = realmContacts.firstOrNull { realmContact ->
realmContact.numbers.any {
phoneNumberUtils.compare(it.address, address)
}
}
}
}
conversation.recipients.clear()
conversation.recipients.addAll(recipients)
conversation.lastMessage = lastMessage
realm.executeTransaction { it.insertOrUpdate(conversation) }
realm.close()
conversation.apply {
recipients.clear()
recipients.addAll(matchedRecipients)
lastMessage = realm.where(Message::class.java)
.equalTo("threadId", threadId)
.sort("date", Sort.DESCENDING)
.findFirst()
}
conversation
realm.executeTransaction { it.insertOrUpdate(conversation) }
}
}
}
}
}