Merge pull request #416 from gavine99/message-send-congruency-chip-monitoring-improve

message send -> convos congruence and chip->convo monitoring improvements
This commit is contained in:
octoshrimpy
2025-04-07 10:28:31 -04:00
committed by GitHub
6 changed files with 402 additions and 471 deletions
@@ -19,46 +19,46 @@
package dev.octoshrimpy.quik.compat
import android.content.Context
import android.database.sqlite.SqliteWrapper
import android.net.Uri
import android.os.Build
import android.provider.BaseColumns
import android.provider.Telephony
import android.text.TextUtils
import android.util.Patterns
import timber.log.Timber
import java.util.regex.Pattern
object TelephonyCompat {
class TelephonyCompat {
companion object {
val THREADS_CONTENT_URI = Telephony.Threads.CONTENT_URI
private val ID_PROJECTION = arrayOf(BaseColumns._ID)
private val ID_PROJECTION = arrayOf(BaseColumns._ID)
private val THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID")
private val THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID")
val NAME_ADDR_EMAIL_PATTERN = Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*")
val NAME_ADDR_EMAIL_PATTERN = Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*")
fun getOrCreateThreadId(context: Context, recipient: String): Long {
return getOrCreateThreadId(context, listOf(recipient))
}
fun getOrCreateThreadId(context: Context, recipients: Collection<String>): Long {
return Telephony.Threads.getOrCreateThreadId(context, recipients.toSet())
}
fun extractAddrSpec(address: String): String {
val match = NAME_ADDR_EMAIL_PATTERN.matcher(address)
return if (match.matches()) {
match.group(2)
} else address
}
fun isEmailAddress(address: String): Boolean {
if (TextUtils.isEmpty(address)) {
return false
fun getOrCreateThreadId(context: Context, recipient: String): Long {
return getOrCreateThreadId(context, listOf(recipient))
}
fun getOrCreateThreadId(context: Context, recipients: Collection<String>): Long {
return Telephony.Threads.getOrCreateThreadId(context, recipients.toSet())
}
fun extractAddrSpec(address: String): String {
val match = NAME_ADDR_EMAIL_PATTERN.matcher(address)
return if (match.matches()) {
match.group(2)
} else address
}
fun isEmailAddress(address: String): Boolean {
if (TextUtils.isEmpty(address)) {
return false
}
val s = extractAddrSpec(address)
val match = Patterns.EMAIL_ADDRESS.matcher(s)
return match.matches()
}
val s = extractAddrSpec(address)
val match = Patterns.EMAIL_ADDRESS.matcher(s)
return match.matches()
}
}
@@ -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) }
}
}
}
}
}
@@ -19,8 +19,6 @@
package dev.octoshrimpy.quik.interactor
import android.content.Context
import dev.octoshrimpy.quik.compat.TelephonyCompat
import dev.octoshrimpy.quik.extensions.mapNotNull
import dev.octoshrimpy.quik.model.Attachment
import dev.octoshrimpy.quik.repository.ConversationRepository
import dev.octoshrimpy.quik.repository.MessageRepository
@@ -44,29 +42,29 @@ class SendMessage @Inject constructor(
)
override fun buildObservable(params: Params): Flowable<*> = Flowable.just(Unit)
.filter { params.addresses.isNotEmpty() }
.doOnNext {
// If a threadId isn't provided, try to obtain one
val threadId = when (params.threadId) {
0L -> TelephonyCompat.getOrCreateThreadId(context, params.addresses.toSet())
else -> params.threadId
}
messageRepo.sendMessage(params.subId, threadId, params.addresses, params.body, params.attachments,
params.delay)
.filter { params.addresses.isNotEmpty() }
.doOnNext {
// If a threadId isn't provided, try to obtain one
val threadId = when (params.threadId) {
0L -> conversationRepo.getOrCreateConversation(params.addresses)?.id ?: 0
else -> params.threadId
}
// delete attachment local files, if any, because they're saved to mms db by now
params.attachments.forEach { it.removeCacheFile() }
// if unable to find or create a conversation (and/or underlying threadId), fail
if (threadId == 0L)
return@doOnNext
params.apply {
messageRepo.sendMessage(subId, threadId, addresses, body, attachments, delay)
}
.mapNotNull {
// If the threadId wasn't provided, then it's probably because it doesn't exist in Realm.
// Sync it now and get the id
when (params.threadId) {
0L -> conversationRepo.getOrCreateConversation(params.addresses)?.id
else -> params.threadId
}
}
.doOnNext { threadId -> conversationRepo.updateConversations(threadId) }
.doOnNext { threadId -> conversationRepo.markUnarchived(threadId) }
.flatMap { updateBadge.buildObservable(Unit) } // Update the widget
conversationRepo.updateConversations(threadId)
conversationRepo.markUnarchived(threadId)
// delete attachment local files, if any, because they're saved to mms db by now
params.attachments.forEach { it.removeCacheFile() }
}
.flatMap { updateBadge.buildObservable(Unit) } // Update the widget
}
@@ -67,15 +67,15 @@ interface ConversationRepository {
fun getRecipient(recipientId: Long): Recipient?
fun getThreadId(recipient: String): Long?
fun getConversation(recipient: String): Conversation?
fun getThreadId(recipients: Collection<String>): Long?
fun getConversation(recipients: Collection<String>): Conversation?
fun getOrCreateConversation(threadId: Long): Conversation?
fun getOrCreateConversation(address: String): Conversation?
fun getOrCreateConversation(addresses: List<String>): Conversation?
fun getOrCreateConversation(addresses: Collection<String>): Conversation?
fun saveDraft(threadId: Long, draft: String)
@@ -92,7 +92,7 @@ interface ConversationRepository {
fun markUnpinned(vararg threadIds: Long)
fun markBlocked(threadIds: List<Long>, blockingClient: Int, blockReason: String?)
fun markBlocked(threadIds: Collection<Long>, blockingClient: Int, blockReason: String?)
fun markUnblocked(vararg threadIds: Long)
@@ -42,8 +42,8 @@ class BlockedNumbersPresenter @Inject constructor(
.observeOn(Schedulers.io())
.doOnNext { id ->
blockingRepo.getBlockedNumber(id)?.address
?.let(conversationRepo::getThreadId)
?.let { threadId -> markUnblocked.execute(listOf(threadId)) }
?.let(conversationRepo::getConversation)
?.let { conversation -> markUnblocked.execute(listOf(conversation.id)) }
}
.doOnNext(blockingRepo::unblockNumber)
.subscribeOn(Schedulers.io())
@@ -156,56 +156,39 @@ class ComposeViewModel @Inject constructor(
newState { copy(attachments = sharedAttachments) }
val initialConversation = threadId.takeIf { it != 0L }
?.let(conversationRepo::getConversationAsync)
?.asObservable()
?: Observable.empty()
?.let(conversationRepo::getConversationAsync)
?.asObservable()
?: Observable.empty()
val selectedConversation = selectedChips
.skipWhile { it.isEmpty() }
.map { chips -> chips.map { it.address } }
.distinctUntilChanged()
.doOnNext { newState { copy(loading = true) } }
.observeOn(Schedulers.io())
.map { addresses -> Pair(conversationRepo.getOrCreateConversation(addresses)?.id ?: 0, addresses) }
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { newState { copy(loading = false) } }
.switchMap { (threadId, addresses) ->
// If we already have this thread in realm, or we're able to obtain it from the
// system, just return that.
threadId.takeIf { it > 0 }?.let {
return@switchMap conversationRepo.getConversationAsync(threadId).asObservable()
}
// Otherwise, we'll monitor the conversations until our expected conversation is created
conversationRepo.getConversations(prefs.unreadAtTop.get()).asObservable()
.filter { it.isLoaded }
.observeOn(Schedulers.io())
.map { conversationRepo.getOrCreateConversation(addresses)?.id ?: 0 }
.observeOn(AndroidSchedulers.mainThread())
.switchMap { actualThreadId ->
when (actualThreadId) {
0L -> Observable.just(Conversation(0))
else -> conversationRepo.getConversationAsync(actualThreadId).asObservable()
}
}
.skipWhile { recipients -> recipients.isEmpty() }
.map { recipients -> recipients.map { it.address } }
.distinctUntilChanged()
.doOnNext { newState { copy(loading = true) } }
.observeOn(Schedulers.io()) // background thread for possible long telephony running
.doOnNext { addresses -> conversationRepo.getOrCreateConversation(addresses) }
.observeOn(AndroidSchedulers.mainThread())
.switchMap { addresses ->
// monitors convos and triggers when wanted convo is present
conversationRepo.getConversations(false)
.asObservable()
.filter { conversations -> conversations.isLoaded }
.mapNotNull { conversationRepo.getConversation(addresses) }
.doOnNext { newState { copy(loading = false) } }
.switchMap { conversation -> conversation.asObservable() }
}
// Merges two potential conversation sources (threadId from constructor and contact selection) into a single
// stream of conversations. If the conversation was deleted, notify the activity to shut down
// Merges two potential conversation sources (constructor threadId and contact selection)
// into a single stream of conversations. If the conversation was deleted, notify the
// activity to shut down
disposables += selectedConversation
.mergeWith(initialConversation)
.filter { conversation -> conversation.isLoaded }
.doOnNext { conversation ->
if (!conversation.isValid) {
newState { copy(hasError = true) }
}
}
.filter { conversation -> conversation.isValid }
.subscribe(conversation::onNext)
.mergeWith(initialConversation)
.filter { it.isLoaded }
.filter { it.isValid.also { if (!it) newState { copy(hasError = true) } } }
.subscribe(conversation::onNext)
if (addresses.isNotEmpty()) {
if (addresses.isNotEmpty())
selectedChips.onNext(addresses.map { address -> Recipient(address = address) })
}
disposables += chipsReducer
.scan(listOf<Recipient>()) { previousState, reducer -> reducer(previousState) }
@@ -1085,8 +1068,12 @@ class ComposeViewModel @Inject constructor(
// Send a message when the send button is clicked, and disable editing mode if it's enabled
view.sendIntent
.observeOn(Schedulers.io())
.withLatestFrom(view.textChangedIntent) { _, body -> body.toString() }
.withLatestFrom(state, conversation, selectedChips) { body, state, conversation, chips ->
.withLatestFrom(
view.textChangedIntent,
state,
conversation,
selectedChips
) { _, body, state, conversation, chips ->
if (!permissionManager.isDefaultSms()) {
view.requestDefaultSms()
return@withLatestFrom
@@ -1115,60 +1102,49 @@ class ComposeViewModel @Inject constructor(
false -> chips.map { chip -> chip.address }
}
val sendAsGroup = ((addresses.size > 1) && // if more than one address to send to
(!state.editingMode || // and is not a new convo
state.sendAsGroup)) // or (is a new convo and) send as group is selected
(!state.editingMode || // and is not a new convo (group msg or not is already set)
state.sendAsGroup)) // or (is a new convo and) send as group is selected
when {
// Scheduling a message
state.scheduled != 0L -> {
state.scheduled != 0L -> addScheduledMessage.execute(
AddScheduledMessage.Params(
state.scheduled,
subId,
addresses,
sendAsGroup,
body.toString(),
state.attachments.map { it.uri.toString() }
)
).also {
newState { copy(scheduled = 0) }
val uris = state.attachments.map { it.uri.toString() }
val params = AddScheduledMessage
.Params(state.scheduled, subId, addresses, sendAsGroup, body, uris)
addScheduledMessage.execute(params)
context.makeToast(R.string.compose_scheduled_toast)
}
// Sending a group message
sendAsGroup -> {
sendMessage.execute(SendMessage
.Params(subId, conversation.id, addresses, body, state.attachments, delay))
}
// sending a group message
sendAsGroup -> sendMessage.execute(
SendMessage.Params(
subId,
0,
addresses,
body.toString(),
state.attachments,
delay
)
)
// Sending a message to an existing conversation with one recipient
conversation.recipients.size == 1 -> {
val address = conversation.recipients.map { it.address }
sendMessage.execute(SendMessage.Params(subId, threadId, address, body, state.attachments, delay))
}
// Create a new conversation with one address
addresses.size == 1 -> {
sendMessage.execute(SendMessage
.Params(subId, threadId, addresses, body, state.attachments, delay))
}
// Send a message to multiple addresses
else -> {
addresses.forEach {
val threadId = tryOrNull(false) {
TelephonyCompat.getOrCreateThreadId(context, it)
} ?: 0
val address = conversationRepo.getOrCreateConversation(it)
?.recipients
?.firstOrNull()
?.address
?: it
sendMessage.execute(
SendMessage.Params(
subId,
threadId,
listOf(address),
body,
state.attachments,
delay
)
// sending message to individual address(es)
else -> addresses.forEach {
sendMessage.execute(
SendMessage.Params(
subId,
0,
listOf(it),
body.toString(),
state.attachments,
delay
)
}
)
}
}
@@ -1176,12 +1152,14 @@ class ComposeViewModel @Inject constructor(
// compose activity)
view.clearCurrentMessageIntent.onNext(
((addresses.size > 1) && // if more than one address to send to
state.editingMode && // and is a new convo
!state.sendAsGroup) // and is *not* sent as a group
)
}
.autoDisposable(view.scope())
.subscribe{view.focusMessage()}
state.editingMode && // and is a new convo
!state.sendAsGroup) // and is *not* sent as a group
)
}
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { view.focusMessage() }
.autoDisposable(view.scope())
.subscribe()
// View QKSMS+
view.viewQksmsPlusIntent