diff --git a/data/src/main/java/mapper/CursorToConversationImpl.kt b/data/src/main/java/mapper/CursorToConversationImpl.kt index 481cc632..fa2a425f 100644 --- a/data/src/main/java/mapper/CursorToConversationImpl.kt +++ b/data/src/main/java/mapper/CursorToConversationImpl.kt @@ -33,20 +33,29 @@ class CursorToConversationImpl @Inject constructor(private val context: Context) val URI: Uri = Uri.parse("content://mms-sms/conversations?simple=true") val PROJECTION = arrayOf( Threads._ID, - Threads.RECIPIENT_IDS) + Threads.DATE, + Threads.RECIPIENT_IDS, + Threads.READ, + Threads.SNIPPET) - val ID = 0 - val RECIPIENT_IDS = 1 + const val ID = 0 + const val DATE = 1 + const val RECIPIENT_IDS = 2 + const val READ = 3 + const val SNIPPET = 4 } override fun map(from: Cursor): Conversation { return Conversation().apply { id = from.getLong(ID) + date = from.getLong(DATE) recipients.addAll(from.getString(RECIPIENT_IDS) .split(" ") .filter { it.isNotBlank() } .map { recipientId -> recipientId.toLong() } .map { recipientId -> Recipient().apply { id = recipientId } }) + read = from.getInt(READ) == 1 + snippet = from.getString(SNIPPET) ?: "" } } diff --git a/data/src/main/java/mapper/CursorToMessageImpl.kt b/data/src/main/java/mapper/CursorToMessageImpl.kt index 0478f4b0..5a5bdd55 100644 --- a/data/src/main/java/mapper/CursorToMessageImpl.kt +++ b/data/src/main/java/mapper/CursorToMessageImpl.kt @@ -24,7 +24,6 @@ import android.net.Uri import android.provider.Telephony.* import com.google.android.mms.pdu_alt.PduHeaders import manager.KeyManager -import model.Conversation import model.Message import util.extensions.map import javax.inject.Inject @@ -77,14 +76,13 @@ class CursorToMessageImpl @Inject constructor( } id = keys.newId() + threadId = cursor.getLong(columnsMap.threadId) contentId = cursor.getLong(columnsMap.msgId) date = cursor.getLong(columnsMap.date) dateSent = cursor.getLong(columnsMap.dateSent) read = cursor.getInt(columnsMap.read) != 0 locked = cursor.getInt(columnsMap.locked) != 0 - conversation = Conversation().apply { id = cursor.getLong(columnsMap.threadId) } - when (type) { "sms" -> { address = cursor.getString(columnsMap.smsAddress) ?: "" diff --git a/data/src/main/java/repository/MessageRepositoryImpl.kt b/data/src/main/java/repository/MessageRepositoryImpl.kt index b9601809..e3faf032 100644 --- a/data/src/main/java/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/repository/MessageRepositoryImpl.kt @@ -63,26 +63,24 @@ class MessageRepositoryImpl @Inject constructor( private val cursorToRecipient: CursorToRecipient, private val prefs: Preferences) : MessageRepository { - override fun getConversations(archived: Boolean): RealmResults { + override fun getConversations(archived: Boolean): RealmResults { return Realm.getDefaultInstance() - .where(Message::class.java) - .distinctValues("threadId") - .notEqualTo("threadId", 0L) - .equalTo("conversation.archived", archived) - .equalTo("conversation.blocked", false) - .isNotEmpty("conversation.recipients") + .where(Conversation::class.java) + .notEqualTo("id", 0L) + .equalTo("archived", archived) + .equalTo("blocked", false) + .isNotEmpty("recipients") .sort("date", Sort.DESCENDING) .findAllAsync() } - override fun getConversationsSnapshot(): List { + override fun getConversationsSnapshot(): List { val realm = Realm.getDefaultInstance() - return realm.copyFromRealm(realm.where(Message::class.java) - .distinctValues("threadId") - .notEqualTo("threadId", 0L) - .equalTo("conversation.archived", false) - .equalTo("conversation.blocked", false) - .isNotEmpty("conversation.recipients") + return realm.copyFromRealm(realm.where(Conversation::class.java) + .notEqualTo("id", 0L) + .equalTo("archived", false) + .equalTo("blocked", false) + .isNotEmpty("recipients") .sort("date", Sort.DESCENDING) .findAll()) } @@ -186,9 +184,9 @@ class MessageRepositoryImpl @Inject constructor( .findFirst() } - override fun getUnreadMessageCount(): Long { + override fun getUnreadCount(): Long { return Realm.getDefaultInstance() - .where(Message::class.java) + .where(Conversation::class.java) .equalTo("read", false) .count() } @@ -234,6 +232,30 @@ class MessageRepositoryImpl @Inject constructor( .findAll() } + override fun updateConversation(threadId: Long) { + Realm.getDefaultInstance().use { realm -> + realm.refresh() + + val conversation = realm + .where(Conversation::class.java) + .equalTo("id", threadId) + .findFirst() ?: return + + val message = realm + .where(Message::class.java) + .equalTo("threadId", threadId) + .sort("date", Sort.DESCENDING) + .findAll().first() ?: return + + realm.executeTransaction { + conversation.date = message.date + conversation.snippet = message.getSummary() + conversation.read = message.read + conversation.me = message.isMe() + } + } + } + override fun markArchived(threadId: Long) { val realm = Realm.getDefaultInstance() val conversation = realm.where(Conversation::class.java) @@ -365,6 +387,7 @@ class MessageRepositoryImpl @Inject constructor( // Insert the message to Realm val message = Message().apply { + this.threadId = threadId this.address = address this.body = body this.date = System.currentTimeMillis() @@ -374,8 +397,6 @@ class MessageRepositoryImpl @Inject constructor( type = "sms" read = true seen = true - - conversation = getOrCreateConversation(threadId) } val realm = Realm.getDefaultInstance() var managedMessage: Message? = null @@ -410,10 +431,9 @@ class MessageRepositoryImpl @Inject constructor( this.date = System.currentTimeMillis() id = messageIds.newId() + threadId = getOrCreateConversation(address).blockingGet().id boxId = Telephony.Sms.MESSAGE_TYPE_INBOX type = "sms" - - conversation = getOrCreateConversation(address).blockingGet() } val realm = Realm.getDefaultInstance() var managedMessage: Message? = null diff --git a/data/src/main/java/repository/SyncRepositoryImpl.kt b/data/src/main/java/repository/SyncRepositoryImpl.kt index c4ab87cb..43a0c364 100644 --- a/data/src/main/java/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/repository/SyncRepositoryImpl.kt @@ -169,10 +169,7 @@ class SyncRepositoryImpl @Inject constructor( return cursor.asFlowable() .map { cursorToMessage.map(Pair(it, columnsMap)) } .doOnNext { message -> existingId?.let { message.id = it } } - .doOnNext { message -> - val threadId = message.conversation?.id - message.conversation = threadId?.let { messageRepo.getOrCreateConversation(threadId) } - } + .doOnNext { message -> messageRepo.getOrCreateConversation(message.threadId) } .doOnNext { message -> message.insertOrUpdate() } } diff --git a/domain/src/main/java/interactor/DeleteMessage.kt b/domain/src/main/java/interactor/DeleteMessage.kt index 3d7a7f6a..afcc5792 100644 --- a/domain/src/main/java/interactor/DeleteMessage.kt +++ b/domain/src/main/java/interactor/DeleteMessage.kt @@ -23,13 +23,18 @@ import repository.MessageRepository import javax.inject.Inject class DeleteMessage @Inject constructor( - private val messageRepository: MessageRepository, + private val messageRepo: MessageRepository, private val updateBadge: UpdateBadge ) : Interactor() { override fun buildObservable(params: Long): Flowable<*> { return Flowable.just(params) - .doOnNext { messageId -> messageRepository.deleteMessage(messageId) } + .doOnNext { messageId -> + val threadId = messageRepo.getMessage(messageId)?.threadId + messageRepo.deleteMessage(messageId) + threadId?.let { messageRepo.updateConversation(it) } + } + .doOnNext { messageRepo.updateConversation(params) } // Update the conversation .flatMap { updateBadge.buildObservable(Unit) } // Update the badge } diff --git a/domain/src/main/java/interactor/MarkRead.kt b/domain/src/main/java/interactor/MarkRead.kt index d7c64e45..aa87e3a5 100644 --- a/domain/src/main/java/interactor/MarkRead.kt +++ b/domain/src/main/java/interactor/MarkRead.kt @@ -32,6 +32,7 @@ class MarkRead @Inject constructor( override fun buildObservable(params: Long): Flowable<*> { return Flowable.just(Unit) .doOnNext { messageRepo.markRead(params) } + .doOnNext { messageRepo.updateConversation(params) } // Update the conversation .doOnNext { notificationManager.update(params) } .flatMap { updateBadge.buildObservable(Unit) } // Update the badge } diff --git a/domain/src/main/java/interactor/ReceiveMms.kt b/domain/src/main/java/interactor/ReceiveMms.kt index 603fafb4..9509b9e2 100644 --- a/domain/src/main/java/interactor/ReceiveMms.kt +++ b/domain/src/main/java/interactor/ReceiveMms.kt @@ -24,6 +24,7 @@ import manager.NotificationManager import repository.MessageRepository import repository.SyncRepository import util.extensions.mapNotNull +import java.util.concurrent.TimeUnit import javax.inject.Inject class ReceiveMms @Inject constructor( @@ -36,10 +37,13 @@ class ReceiveMms @Inject constructor( override fun buildObservable(params: Uri): Flowable<*> { return Flowable.just(params) .flatMap { uri -> syncManager.syncMessage(uri) } // Sync the message - .mapNotNull { message -> message.conversation } // Map message to conversation + .doOnNext { message -> messageRepo.updateConversation(message.threadId) } // Update the conversation + .mapNotNull { message -> messageRepo.getOrCreateConversation(message.threadId) } // Map message to conversation .filter { conversation -> !conversation.blocked } // Don't notify for blocked conversations .doOnNext { conversation -> if (conversation.archived) messageRepo.markUnarchived(conversation.id) } // Unarchive conversation if necessary - .doOnNext { conversation -> notificationManager.update(conversation.id) } // Update the notification + .map { conversation -> conversation.id } // Map to the id because [delay] will put us on the wrong thread + .delay(1, TimeUnit.SECONDS) // Wait one second before trying to notify, in case the foreground app marks it as read first + .doOnNext { threadId -> notificationManager.update(threadId) } // Update the notification .flatMap { updateBadge.buildObservable(Unit) } // Update the badge } diff --git a/domain/src/main/java/interactor/ReceiveSms.kt b/domain/src/main/java/interactor/ReceiveSms.kt index 5fbf1a92..3dbba824 100644 --- a/domain/src/main/java/interactor/ReceiveSms.kt +++ b/domain/src/main/java/interactor/ReceiveSms.kt @@ -44,11 +44,13 @@ class ReceiveSms @Inject constructor( messageRepo.insertReceivedSms(address, body, time) // Add the message to the db } - .mapNotNull { message -> message.conversation } // Map message to conversation + .doOnNext { message -> messageRepo.updateConversation(message.threadId) } // Update the conversation + .mapNotNull { message -> messageRepo.getOrCreateConversation(message.threadId) } // Map message to conversation .filter { conversation -> !conversation.blocked } // Don't notify for blocked conversations .doOnNext { conversation -> if (conversation.archived) messageRepo.markUnarchived(conversation.id) } // Unarchive conversation if necessary + .map { conversation -> conversation.id } // Map to the id because [delay] will put us on the wrong thread .delay(1, TimeUnit.SECONDS) // Wait one second before trying to notify, in case the foreground app marks it as read first - .doOnNext { conversation -> notificationManager.update(conversation.id) } // Update the notification + .doOnNext { threadId -> notificationManager.update(threadId) } // Update the notification .flatMap { updateBadge.buildObservable(Unit) } // Update the badge } diff --git a/domain/src/main/java/interactor/UpdateBadge.kt b/domain/src/main/java/interactor/UpdateBadge.kt index 4363be25..68774094 100644 --- a/domain/src/main/java/interactor/UpdateBadge.kt +++ b/domain/src/main/java/interactor/UpdateBadge.kt @@ -30,7 +30,7 @@ class UpdateBadge @Inject constructor( override fun buildObservable(params: Unit): Flowable<*> { return Flowable.just(params) - .map { messageRepo.getUnreadMessageCount() } + .map { messageRepo.getUnreadCount() } .map { count -> count.toInt() } .doOnNext { count -> widgetManager.updateUnreadCount(count) } } diff --git a/domain/src/main/java/model/Conversation.kt b/domain/src/main/java/model/Conversation.kt index efb8cf45..e5157001 100644 --- a/domain/src/main/java/model/Conversation.kt +++ b/domain/src/main/java/model/Conversation.kt @@ -28,9 +28,13 @@ import java.util.* open class Conversation : RealmObject() { @PrimaryKey var id: Long = 0 - var recipients: RealmList = RealmList() @Index var archived: Boolean = false @Index var blocked: Boolean = false + var recipients: RealmList = RealmList() + var date: Long = 0 + var snippet: String = "" + var read: Boolean = true + var me: Boolean = false var draft: String = "" fun getTitle() = recipients.joinToString { recipient -> diff --git a/domain/src/main/java/model/Message.kt b/domain/src/main/java/model/Message.kt index 419187e4..b3e3536a 100644 --- a/domain/src/main/java/model/Message.kt +++ b/domain/src/main/java/model/Message.kt @@ -32,19 +32,7 @@ open class Message : RealmObject() { @PrimaryKey var id: Long = 0 - var conversation: Conversation? = null - set(value) { - field = value - threadId = value?.id ?: 0 - } - - /** - * This is a hack so that we can index by the threadId, and return distinct threadIds when - * querying for all conversations for the MainActivity - */ - @Index - @Deprecated("This is only here for Realm to access - it should be ignored in our codebase") - var threadId: Long = 0 + @Index var threadId: Long = 0 // The MMS-SMS content provider returns messages where duplicate ids can exist. This is because // SMS and MMS are stored in separate tables. We can't use these ids as our realm message id @@ -136,8 +124,7 @@ open class Message : RealmObject() { .mapNotNull { it.text } .joinToString("\n") { text -> text } .takeIf { it.isNotEmpty() } - ?: getCleansedSubject() - .takeIf { it.isNotEmpty() } + ?: getCleansedSubject().takeIf { it.isNotEmpty() } ?: "An MMS message" } } diff --git a/domain/src/main/java/repository/MessageRepository.kt b/domain/src/main/java/repository/MessageRepository.kt index 259e42f1..69c81e89 100644 --- a/domain/src/main/java/repository/MessageRepository.kt +++ b/domain/src/main/java/repository/MessageRepository.kt @@ -28,9 +28,9 @@ import model.MmsPart interface MessageRepository { - fun getConversations(archived: Boolean = false): RealmResults + fun getConversations(archived: Boolean = false): RealmResults - fun getConversationsSnapshot(): List + fun getConversationsSnapshot(): List fun getBlockedConversations(): Flowable> @@ -52,7 +52,7 @@ interface MessageRepository { fun getMessageForPart(id: Long): Message? - fun getUnreadMessageCount(): Long + fun getUnreadCount(): Long fun getPart(id: Long): MmsPart? @@ -70,6 +70,11 @@ interface MessageRepository { */ fun getUnreadMessages(threadId: Long): RealmResults + /** + * Updates message-related fields in the conversation, like the date and snippet + */ + fun updateConversation(threadId: Long) + fun markArchived(threadId: Long) fun markUnarchived(threadId: Long) diff --git a/presentation/src/main/java/common/util/NotificationManagerImpl.kt b/presentation/src/main/java/common/util/NotificationManagerImpl.kt index f0427299..94f151e3 100644 --- a/presentation/src/main/java/common/util/NotificationManagerImpl.kt +++ b/presentation/src/main/java/common/util/NotificationManagerImpl.kt @@ -154,7 +154,7 @@ class NotificationManagerImpl @Inject constructor( return } - val conversation = message.conversation ?: return + val conversation = messageRepo.getConversation(message.threadId) ?: return val threadId = conversation.id val contentIntent = Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId) diff --git a/presentation/src/main/java/common/util/filter/ConversationFilter.kt b/presentation/src/main/java/common/util/filter/ConversationFilter.kt index 4e3acc0d..fcc0bdbf 100644 --- a/presentation/src/main/java/common/util/filter/ConversationFilter.kt +++ b/presentation/src/main/java/common/util/filter/ConversationFilter.kt @@ -18,13 +18,13 @@ */ package common.util.filter -import model.Message +import model.Conversation import javax.inject.Inject -class ConversationFilter @Inject constructor(private val recipientFilter: RecipientFilter) : Filter() { +class ConversationFilter @Inject constructor(private val recipientFilter: RecipientFilter) : Filter() { - override fun filter(item: Message, query: CharSequence): Boolean { - return item.conversation?.recipients?.any { recipient -> recipientFilter.filter(recipient, query) } ?: false + override fun filter(item: Conversation, query: CharSequence): Boolean { + return item.recipients.any { recipient -> recipientFilter.filter(recipient, query) } } } \ No newline at end of file diff --git a/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt b/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt index 97a7e02f..c5460219 100644 --- a/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt @@ -19,7 +19,6 @@ package feature.conversations import android.content.Context -import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.ViewGroup import com.jakewharton.rxbinding2.view.clicks @@ -35,7 +34,7 @@ import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import kotlinx.android.synthetic.main.conversation_list_item.view.* import model.Contact -import model.Message +import model.Conversation import model.PhoneNumber import javax.inject.Inject @@ -43,13 +42,12 @@ class ConversationsAdapter @Inject constructor( private val context: Context, private val dateFormatter: DateFormatter, private val colors: Colors -) : QkRealmAdapter() { +) : QkRealmAdapter() { val clicks: Subject = PublishSubject.create() val longClicks: Subject = PublishSubject.create() private val disposables = CompositeDisposable() - private var attachedRecyclerViews = 0 init { setHasStableIds(true) @@ -72,22 +70,8 @@ class ConversationsAdapter @Inject constructor( return QkViewHolder(view) } - fun isAttachedToRecyclerView() = attachedRecyclerViews > 0 - - override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { - super.onAttachedToRecyclerView(recyclerView) - attachedRecyclerViews++ - } - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { - super.onDetachedFromRecyclerView(recyclerView) - attachedRecyclerViews-- - disposables.clear() - } - override fun onBindViewHolder(viewHolder: QkViewHolder, position: Int) { - val message = getItem(position)!! - val conversation = message.conversation!! + val conversation = getItem(position)!! val view = viewHolder.itemView view.clicks().subscribe { clicks.onNext(conversation.id) } @@ -97,12 +81,12 @@ class ConversationsAdapter @Inject constructor( recipient.contact ?: Contact().apply { numbers.add(PhoneNumber().apply { address = recipient.address }) } } view.title.text = conversation.getTitle() - view.date.text = dateFormatter.getConversationTimestamp(message.date) - view.snippet.text = if (message.isMe()) "You: ${message.getSummary()}" else message.getSummary() + view.date.text = dateFormatter.getConversationTimestamp(conversation.date) + view.snippet.text = if (conversation.me) "You: ${conversation.snippet}" else conversation.snippet } override fun getItemId(index: Int): Long { - return getItem(index)!!.conversation!!.id + return getItem(index)!!.id } override fun getItemViewType(position: Int): Int { diff --git a/presentation/src/main/java/feature/gallery/GalleryViewModel.kt b/presentation/src/main/java/feature/gallery/GalleryViewModel.kt index 61aa5546..48759451 100644 --- a/presentation/src/main/java/feature/gallery/GalleryViewModel.kt +++ b/presentation/src/main/java/feature/gallery/GalleryViewModel.kt @@ -54,7 +54,7 @@ class GalleryViewModel(intent: Intent) : QkViewModel( disposables += partIdFlowable .mapNotNull { partId -> messageRepo.getMessageForPart(partId) } - .mapNotNull { message -> message.conversation } + .mapNotNull { message -> messageRepo.getConversation(message.threadId) } .subscribe { conversation -> newState { it.copy(title = conversation.getTitle()) } } } diff --git a/presentation/src/main/java/feature/main/MainState.kt b/presentation/src/main/java/feature/main/MainState.kt index bffa1d2b..492effeb 100644 --- a/presentation/src/main/java/feature/main/MainState.kt +++ b/presentation/src/main/java/feature/main/MainState.kt @@ -20,7 +20,7 @@ package feature.main import common.MenuItem import io.realm.RealmResults -import model.Message +import model.Conversation data class MainState( val page: MainPage = Inbox(), @@ -32,12 +32,12 @@ sealed class MainPage data class Inbox( val showClearButton: Boolean = false, - val data: RealmResults? = null, + val data: RealmResults? = null, val menu: List = ArrayList(), val showArchivedSnackbar: Boolean = false) : MainPage() data class Archived( - val data: RealmResults? = null, + val data: RealmResults? = null, val menu: List = ArrayList()) : MainPage() data class Scheduled( diff --git a/presentation/src/main/java/feature/widget/WidgetAdapter.kt b/presentation/src/main/java/feature/widget/WidgetAdapter.kt index a16837b4..2b643f59 100644 --- a/presentation/src/main/java/feature/widget/WidgetAdapter.kt +++ b/presentation/src/main/java/feature/widget/WidgetAdapter.kt @@ -38,7 +38,7 @@ import common.util.extensions.dpToPx import feature.main.MainActivity import injection.appComponent import model.Contact -import model.Message +import model.Conversation import model.PhoneNumber import repository.MessageRepository import javax.inject.Inject @@ -56,7 +56,7 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { private val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) private val smallWidget = intent.getBooleanExtra("small_widget", false) - private var conversations: List = listOf() + private var conversations: List = listOf() private val appWidgetManager by lazy { AppWidgetManager.getInstance(context) } // Cache colors, load lazily. This entire class is recreated when the widget is recreated, @@ -97,8 +97,7 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { } private fun getConversationView(position: Int): RemoteViews { - val message = conversations[position] - val conversation = message.conversation!! + val conversation = conversations[position] val remoteViews = RemoteViews(context.packageName, R.layout.widget_list_item) @@ -138,15 +137,15 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { // Name remoteViews.setTextColor(R.id.name, textPrimary) - remoteViews.setTextViewText(R.id.name, boldText(conversation.getTitle(), !message.read)) + remoteViews.setTextViewText(R.id.name, boldText(conversation.getTitle(), !conversation.read)) // Date - remoteViews.setTextColor(R.id.date, if (message.read) textTertiary else textPrimary) - remoteViews.setTextViewText(R.id.date, boldText(dateFormatter.getConversationTimestamp(message.date), !message.read)) + remoteViews.setTextColor(R.id.date, if (conversation.read) textTertiary else textPrimary) + remoteViews.setTextViewText(R.id.date, boldText(dateFormatter.getConversationTimestamp(conversation.date), !conversation.read)) // Snippet - remoteViews.setTextColor(R.id.snippet, if (message.read) textTertiary else textPrimary) - remoteViews.setTextViewText(R.id.snippet, boldText(message.getSummary(), !message.read)) + remoteViews.setTextColor(R.id.snippet, if (conversation.read) textTertiary else textPrimary) + remoteViews.setTextViewText(R.id.snippet, boldText(conversation.snippet, !conversation.read)) // Launch conversation on click val clickIntent = Intent().putExtra("threadId", conversation.id) diff --git a/presentation/src/main/java/interactor/SendMessage.kt b/presentation/src/main/java/interactor/SendMessage.kt index a017c6bf..0dbaaaaf 100644 --- a/presentation/src/main/java/interactor/SendMessage.kt +++ b/presentation/src/main/java/interactor/SendMessage.kt @@ -49,6 +49,7 @@ class SendMessage @Inject constructor( sendMms(params.threadId, params.addresses, params.body, params.attachments) } } + .doOnNext { messageRepo.updateConversation(params.threadId) } .doOnNext { messageRepo.markUnarchived(params.threadId) } }