diff --git a/data/src/main/java/repository/MessageRepositoryImpl.kt b/data/src/main/java/repository/MessageRepositoryImpl.kt index 32d77ac6..6baf96d4 100644 --- a/data/src/main/java/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/repository/MessageRepositoryImpl.kt @@ -48,6 +48,7 @@ import model.Message import model.MmsPart import util.MessageUtils import util.Preferences +import util.extensions.anyOf import util.extensions.asMaybe import util.extensions.insertOrUpdate import util.extensions.map @@ -236,71 +237,82 @@ class MessageRepositoryImpl @Inject constructor( .findAll() } - override fun updateConversation(threadId: Long) { + override fun updateConversations(vararg threadIds: Long) { Realm.getDefaultInstance().use { realm -> realm.refresh() - val conversation = realm - .where(Conversation::class.java) - .equalTo("id", threadId) - .findFirst() ?: return + threadIds.forEach { threadId -> + val conversation = realm + .where(Conversation::class.java) + .equalTo("id", threadId) + .findFirst() ?: return - val messages = realm - .where(Message::class.java) - .equalTo("threadId", threadId) - .sort("date", Sort.DESCENDING) - .findAll() + val messages = realm + .where(Message::class.java) + .equalTo("threadId", threadId) + .sort("date", Sort.DESCENDING) + .findAll() - val message = messages.firstOrNull() + val message = messages.firstOrNull() - realm.executeTransaction { - conversation.count = messages.size - conversation.date = message?.date ?: 0 - conversation.snippet = message?.getSummary() ?: "" - conversation.read = message?.read ?: true - conversation.me = message?.isMe() ?: false + realm.executeTransaction { + conversation.count = messages.size + conversation.date = message?.date ?: 0 + conversation.snippet = message?.getSummary() ?: "" + conversation.read = message?.read ?: true + conversation.me = message?.isMe() ?: false + } } } } - override fun markArchived(threadId: Long) { - val realm = Realm.getDefaultInstance() - val conversation = realm.where(Conversation::class.java) - .equalTo("id", threadId) - .findFirst() + override fun markArchived(vararg threadIds: Long) { + Realm.getDefaultInstance().use { realm -> + val conversations = realm.where(Conversation::class.java) + .anyOf("id", threadIds) + .findAll() - realm.executeTransaction { conversation?.archived = true } - realm.close() + realm.executeTransaction { + conversations.forEach { it.archived = true } + } + } } - override fun markUnarchived(threadId: Long) { - val realm = Realm.getDefaultInstance() - val conversation = realm.where(Conversation::class.java) - .equalTo("id", threadId) - .findFirst() + override fun markUnarchived(vararg threadIds: Long) { + Realm.getDefaultInstance().use { realm -> + val conversations = realm.where(Conversation::class.java) + .anyOf("id", threadIds) + .findAll() - realm.executeTransaction { conversation?.archived = false } - realm.close() + realm.executeTransaction { + conversations.forEach { it.archived = false } + } + } } - override fun markBlocked(threadId: Long) { - val realm = Realm.getDefaultInstance() - val conversation = realm.where(Conversation::class.java) - .equalTo("id", threadId) - .findFirst() + override fun markBlocked(vararg threadIds: Long) { + Realm.getDefaultInstance().use { realm -> + val conversations = realm.where(Conversation::class.java) + .anyOf("id", threadIds) + .findAll() - realm.executeTransaction { conversation?.blocked = true } - realm.close() + realm.executeTransaction { + conversations.forEach { it.blocked = true } + } + } } - override fun markUnblocked(threadId: Long) { - val realm = Realm.getDefaultInstance() - val conversation = realm.where(Conversation::class.java) - .equalTo("id", threadId) - .findFirst() + override fun markUnblocked(vararg threadIds: Long) { + Realm.getDefaultInstance().use { realm -> + val conversations = realm.where(Conversation::class.java) + .anyOf("id", threadIds) + .equalTo("archived", false) + .findAll() - realm.executeTransaction { conversation?.blocked = false } - realm.close() + realm.executeTransaction { + conversations.forEach { it.blocked = false } + } + } } override fun markAllSeen() { @@ -611,10 +623,10 @@ class MessageRepositoryImpl @Inject constructor( } } - override fun deleteConversation(threadId: Long) { + override fun deleteConversations(vararg threadIds: Long) { Realm.getDefaultInstance().use { realm -> - val conversation = realm.where(Conversation::class.java).equalTo("id", threadId).findAll() - val messages = realm.where(Message::class.java).equalTo("threadId", threadId).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() @@ -622,8 +634,10 @@ class MessageRepositoryImpl @Inject constructor( } } - val uri = ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId) - context.contentResolver.delete(uri, null, null) + threadIds.forEach { threadId -> + val uri = ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId) + context.contentResolver.delete(uri, null, null) + } } private fun getConversationFromCp(threadId: Long): Conversation? { diff --git a/data/src/main/java/util/extensions/RealmExtensions.kt b/data/src/main/java/util/extensions/RealmExtensions.kt index d612f65e..88d2d0a0 100644 --- a/data/src/main/java/util/extensions/RealmExtensions.kt +++ b/data/src/main/java/util/extensions/RealmExtensions.kt @@ -22,7 +22,9 @@ import io.reactivex.Observable import io.realm.Realm import io.realm.RealmModel import io.realm.RealmObject +import io.realm.RealmQuery import io.realm.RealmResults +import timber.log.Timber fun RealmModel.insertOrUpdate() { val realm = Realm.getDefaultInstance() @@ -42,4 +44,13 @@ fun RealmObject.asObservable(): Observable { fun RealmResults.asObservable(): Observable> { return asFlowable().toObservable() +} + +fun RealmQuery.anyOf(fieldName: String, values: LongArray): RealmQuery { + values.forEach { Timber.v("vararg: $it") } + + return when (values.isEmpty()) { + true -> equalTo(fieldName, -1L) + false -> `in`(fieldName, values.toTypedArray()) + } } \ No newline at end of file diff --git a/domain/src/main/java/interactor/DeleteConversation.kt b/domain/src/main/java/interactor/DeleteConversations.kt similarity index 78% rename from domain/src/main/java/interactor/DeleteConversation.kt rename to domain/src/main/java/interactor/DeleteConversations.kt index 94e08fe2..ba9bdec9 100644 --- a/domain/src/main/java/interactor/DeleteConversation.kt +++ b/domain/src/main/java/interactor/DeleteConversations.kt @@ -22,14 +22,14 @@ import io.reactivex.Flowable import repository.MessageRepository import javax.inject.Inject -class DeleteConversation @Inject constructor( +class DeleteConversations @Inject constructor( private val messageRepo: MessageRepository, private val updateBadge: UpdateBadge -): Interactor() { +) : Interactor>() { - override fun buildObservable(params: Long): Flowable<*> { - return Flowable.just(Unit) - .doOnNext { messageRepo.deleteConversation(params) } + override fun buildObservable(params: List): Flowable<*> { + return Flowable.just(params.toLongArray()) + .doOnNext { threadIds -> messageRepo.deleteConversations(*threadIds) } .flatMap { updateBadge.buildObservable(Unit) } // Update the badge } diff --git a/domain/src/main/java/interactor/DeleteMessage.kt b/domain/src/main/java/interactor/DeleteMessage.kt index afcc5792..0590cb7b 100644 --- a/domain/src/main/java/interactor/DeleteMessage.kt +++ b/domain/src/main/java/interactor/DeleteMessage.kt @@ -32,9 +32,9 @@ class DeleteMessage @Inject constructor( .doOnNext { messageId -> val threadId = messageRepo.getMessage(messageId)?.threadId messageRepo.deleteMessage(messageId) - threadId?.let { messageRepo.updateConversation(it) } + threadId?.let { messageRepo.updateConversations(it) } } - .doOnNext { messageRepo.updateConversation(params) } // Update the conversation + .doOnNext { messageRepo.updateConversations(params) } // Update the conversation .flatMap { updateBadge.buildObservable(Unit) } // Update the badge } diff --git a/domain/src/main/java/interactor/MarkArchived.kt b/domain/src/main/java/interactor/MarkArchived.kt index 0c587b45..d8b38b9f 100644 --- a/domain/src/main/java/interactor/MarkArchived.kt +++ b/domain/src/main/java/interactor/MarkArchived.kt @@ -22,11 +22,11 @@ import io.reactivex.Flowable import repository.MessageRepository import javax.inject.Inject -class MarkArchived @Inject constructor(private val messageRepo: MessageRepository) : Interactor() { +class MarkArchived @Inject constructor(private val messageRepo: MessageRepository) : Interactor>() { - override fun buildObservable(params: Long): Flowable { - return Flowable.just(Unit) - .doOnNext { messageRepo.markArchived(params) } + override fun buildObservable(params: List): Flowable<*> { + return Flowable.just(params.toLongArray()) + .doOnNext { threadIds -> messageRepo.markArchived(*threadIds) } } } \ No newline at end of file diff --git a/domain/src/main/java/interactor/MarkBlocked.kt b/domain/src/main/java/interactor/MarkBlocked.kt index e4eb3f8e..a1ff6220 100644 --- a/domain/src/main/java/interactor/MarkBlocked.kt +++ b/domain/src/main/java/interactor/MarkBlocked.kt @@ -22,11 +22,11 @@ import io.reactivex.Flowable import repository.MessageRepository import javax.inject.Inject -class MarkBlocked @Inject constructor(private val messageRepo: MessageRepository) : Interactor() { +class MarkBlocked @Inject constructor(private val messageRepo: MessageRepository) : Interactor>() { - override fun buildObservable(params: Long): Flowable { - return Flowable.just(Unit) - .doOnNext { messageRepo.markBlocked(params) } + override fun buildObservable(params: List): Flowable<*> { + return Flowable.just(params.toLongArray()) + .doOnNext { threadIds -> messageRepo.markBlocked(*threadIds) } } } \ No newline at end of file diff --git a/domain/src/main/java/interactor/MarkRead.kt b/domain/src/main/java/interactor/MarkRead.kt index aa87e3a5..026afa5e 100644 --- a/domain/src/main/java/interactor/MarkRead.kt +++ b/domain/src/main/java/interactor/MarkRead.kt @@ -32,7 +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 { messageRepo.updateConversations(params) } // Update the conversation .doOnNext { notificationManager.update(params) } .flatMap { updateBadge.buildObservable(Unit) } // Update the badge } diff --git a/domain/src/main/java/interactor/MarkUnarchived.kt b/domain/src/main/java/interactor/MarkUnarchived.kt index 4d1a8a26..d7ce0b8c 100644 --- a/domain/src/main/java/interactor/MarkUnarchived.kt +++ b/domain/src/main/java/interactor/MarkUnarchived.kt @@ -22,11 +22,11 @@ import io.reactivex.Flowable import repository.MessageRepository import javax.inject.Inject -class MarkUnarchived @Inject constructor(private val messageRepo: MessageRepository) : Interactor() { +class MarkUnarchived @Inject constructor(private val messageRepo: MessageRepository) : Interactor>() { - override fun buildObservable(params: Long): Flowable { - return Flowable.just(Unit) - .doOnNext { messageRepo.markUnarchived(params) } + override fun buildObservable(params: List): Flowable<*> { + return Flowable.just(params.toLongArray()) + .doOnNext { threadIds -> messageRepo.markUnarchived(*threadIds) } } } \ No newline at end of file diff --git a/domain/src/main/java/interactor/MarkUnblocked.kt b/domain/src/main/java/interactor/MarkUnblocked.kt index 8b82e021..ba7b5aec 100644 --- a/domain/src/main/java/interactor/MarkUnblocked.kt +++ b/domain/src/main/java/interactor/MarkUnblocked.kt @@ -22,11 +22,11 @@ import io.reactivex.Flowable import repository.MessageRepository import javax.inject.Inject -class MarkUnblocked @Inject constructor(private val messageRepo: MessageRepository) : Interactor() { +class MarkUnblocked @Inject constructor(private val messageRepo: MessageRepository) : Interactor>() { - override fun buildObservable(params: Long): Flowable { - return Flowable.just(Unit) - .doOnNext { messageRepo.markUnblocked(params) } + override fun buildObservable(params: List): Flowable<*> { + return Flowable.just(params.toLongArray()) + .doOnNext { threadIds -> messageRepo.markUnblocked(*threadIds) } } } \ No newline at end of file diff --git a/domain/src/main/java/interactor/ReceiveMms.kt b/domain/src/main/java/interactor/ReceiveMms.kt index 1b357233..e34ef5e2 100644 --- a/domain/src/main/java/interactor/ReceiveMms.kt +++ b/domain/src/main/java/interactor/ReceiveMms.kt @@ -48,7 +48,7 @@ class ReceiveMms @Inject constructor( if (blocked) messageRepo.deleteMessage(message.id) } } - .doOnNext { message -> messageRepo.updateConversation(message.threadId) } // Update the conversation + .doOnNext { message -> messageRepo.updateConversations(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 diff --git a/domain/src/main/java/interactor/ReceiveSms.kt b/domain/src/main/java/interactor/ReceiveSms.kt index 62edd92c..411951fd 100644 --- a/domain/src/main/java/interactor/ReceiveSms.kt +++ b/domain/src/main/java/interactor/ReceiveSms.kt @@ -50,7 +50,7 @@ class ReceiveSms @Inject constructor( messageRepo.insertReceivedSms(address, body, time) // Add the message to the db } - .doOnNext { message -> messageRepo.updateConversation(message.threadId) } // Update the conversation + .doOnNext { message -> messageRepo.updateConversations(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 diff --git a/domain/src/main/java/interactor/SyncMessage.kt b/domain/src/main/java/interactor/SyncMessage.kt index 4dc4f915..55f6924c 100644 --- a/domain/src/main/java/interactor/SyncMessage.kt +++ b/domain/src/main/java/interactor/SyncMessage.kt @@ -34,7 +34,7 @@ class SyncMessage @Inject constructor( override fun buildObservable(params: Uri): Flowable { return Flowable.just(params) .mapNotNull { uri -> syncManager.syncMessage(uri) } - .doOnNext { message -> messageRepo.updateConversation(message.threadId) } + .doOnNext { message -> messageRepo.updateConversations(message.threadId) } } } \ No newline at end of file diff --git a/domain/src/main/java/repository/MessageRepository.kt b/domain/src/main/java/repository/MessageRepository.kt index 4a660f1d..87bab362 100644 --- a/domain/src/main/java/repository/MessageRepository.kt +++ b/domain/src/main/java/repository/MessageRepository.kt @@ -72,15 +72,15 @@ interface MessageRepository { /** * Updates message-related fields in the conversation, like the date and snippet */ - fun updateConversation(threadId: Long) + fun updateConversations(vararg threadIds: Long) - fun markArchived(threadId: Long) + fun markArchived(vararg threadIds: Long) - fun markUnarchived(threadId: Long) + fun markUnarchived(vararg threadIds: Long) - fun markBlocked(threadId: Long) + fun markBlocked(vararg threadIds: Long) - fun markUnblocked(threadId: Long) + fun markUnblocked(vararg threadIds: Long) fun markAllSeen() @@ -122,6 +122,6 @@ interface MessageRepository { fun deleteMessage(messageId: Long) - fun deleteConversation(threadId: Long) + fun deleteConversations(vararg threadIds: Long) } \ No newline at end of file diff --git a/presentation/src/main/java/common/base/QkActivity.kt b/presentation/src/main/java/common/base/QkActivity.kt index bb618130..1dce0e3f 100644 --- a/presentation/src/main/java/common/base/QkActivity.kt +++ b/presentation/src/main/java/common/base/QkActivity.kt @@ -61,6 +61,10 @@ abstract class QkActivity> : AppCompatActivity() { title = title // The title may have been set before layout inflation } + override fun setTitle(titleId: Int) { + title = getString(titleId) + } + override fun setTitle(title: CharSequence?) { super.setTitle(title) toolbarTitle?.text = title @@ -74,7 +78,7 @@ abstract class QkActivity> : AppCompatActivity() { return result } - protected fun showBackButton(show: Boolean) { + protected open fun showBackButton(show: Boolean) { supportActionBar?.setDisplayHomeAsUpEnabled(show) } diff --git a/presentation/src/main/java/common/base/QkAdapter.kt b/presentation/src/main/java/common/base/QkAdapter.kt index 7814f803..ee9d8db9 100644 --- a/presentation/src/main/java/common/base/QkAdapter.kt +++ b/presentation/src/main/java/common/base/QkAdapter.kt @@ -20,6 +20,8 @@ package common.base import android.support.v7.util.DiffUtil import android.support.v7.widget.RecyclerView +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject /** * Base RecyclerView.Adapter that provides some convenience when creating a new Adapter, such as @@ -37,6 +39,38 @@ abstract class QkAdapter : RecyclerView.Adapter() { onDatasetChanged() } + val selectionChanges: Subject> = BehaviorSubject.create() + + private val selection = mutableListOf() + + /** + * Toggles the selected state for a particular view + * + * If we are currently in selection mode (we have an active selection), then the state will + * toggle. If we are not in selection mode, then we will only toggle if [force] + */ + protected fun toggleSelection(id: Long, force: Boolean = true): Boolean { + if (!force && selection.isEmpty()) return false + + when (selection.contains(id)) { + true -> selection.remove(id) + false -> selection.add(id) + } + + selectionChanges.onNext(selection) + return true + } + + protected fun isSelected(id: Long): Boolean { + return selection.contains(id) + } + + fun clearSelection() { + selection.clear() + selectionChanges.onNext(selection) + notifyDataSetChanged() + } + fun getItem(position: Int): T { return data[position] } diff --git a/presentation/src/main/java/common/util/Colors.kt b/presentation/src/main/java/common/util/Colors.kt index 3f6537a0..a18d9175 100644 --- a/presentation/src/main/java/common/util/Colors.kt +++ b/presentation/src/main/java/common/util/Colors.kt @@ -159,6 +159,15 @@ class Colors @Inject constructor(private val context: Context, private val prefs .map { res -> getColor(res) } .distinctUntilChanged() + val ripple: Observable = prefs.night.asObservable() + .map { night -> + when (night) { + true -> R.drawable.ripple_dark + false -> R.drawable.ripple + } + } + .distinctUntilChanged() + val field: Observable = prefs.night.asObservable() .map { night -> if (night) R.color.fieldDark else R.color.fieldLight } .map { res -> getColor(res) } diff --git a/presentation/src/main/java/feature/blocked/BlockedViewModel.kt b/presentation/src/main/java/feature/blocked/BlockedViewModel.kt index cd3f6f04..593fac4a 100644 --- a/presentation/src/main/java/feature/blocked/BlockedViewModel.kt +++ b/presentation/src/main/java/feature/blocked/BlockedViewModel.kt @@ -72,7 +72,7 @@ class BlockedViewModel : QkViewModel(BlockedState()) view.confirmUnblockIntent .withLatestFrom(view.unblockIntent, { _, threadId -> threadId }) .autoDisposable(view.scope()) - .subscribe { threadId -> markUnblocked.execute(threadId) } + .subscribe { threadId -> markUnblocked.execute(listOf(threadId)) } } } \ No newline at end of file diff --git a/presentation/src/main/java/feature/conversationinfo/ConversationInfoViewModel.kt b/presentation/src/main/java/feature/conversationinfo/ConversationInfoViewModel.kt index 437c56d2..5bec2fc6 100644 --- a/presentation/src/main/java/feature/conversationinfo/ConversationInfoViewModel.kt +++ b/presentation/src/main/java/feature/conversationinfo/ConversationInfoViewModel.kt @@ -25,7 +25,7 @@ import com.uber.autodispose.kotlin.autoDisposable import common.Navigator import common.base.QkViewModel import injection.appComponent -import interactor.DeleteConversation +import interactor.DeleteConversations import interactor.MarkArchived import interactor.MarkBlocked import interactor.MarkUnarchived @@ -48,7 +48,7 @@ class ConversationInfoViewModel(intent: Intent) : QkViewModel @@ -76,7 +76,7 @@ class ConversationInfoViewModel(intent: Intent) : QkViewModel when (conversation.archived) { - true -> markUnarchived.execute(conversation.id) - false -> markArchived.execute(conversation.id) + true -> markUnarchived.execute(listOf(conversation.id)) + false -> markArchived.execute(listOf(conversation.id)) } } @@ -129,8 +129,8 @@ class ConversationInfoViewModel(intent: Intent) : QkViewModel when (conversation.blocked) { - true -> markUnblocked.execute(conversation.id) - false -> markBlocked.execute(conversation.id) + true -> markUnblocked.execute(listOf(conversation.id)) + false -> markBlocked.execute(listOf(conversation.id)) } } @@ -143,7 +143,7 @@ class ConversationInfoViewModel(intent: Intent) : QkViewModel conversation }) .autoDisposable(view.scope()) - .subscribe { conversation -> deleteConversation.execute(conversation.id) } + .subscribe { conversation -> deleteConversations.execute(listOf(conversation.id)) } } } \ 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 f9419e22..cb715b3e 100644 --- a/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt @@ -20,29 +20,31 @@ package feature.conversations import android.content.Context import android.graphics.Typeface +import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.ViewGroup import com.jakewharton.rxbinding2.view.clicks import com.jakewharton.rxbinding2.view.longClicks import com.moez.QKSMS.R +import common.Navigator import common.base.FlowableAdapter import common.base.QkViewHolder import common.util.Colors import common.util.DateFormatter -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign import kotlinx.android.synthetic.main.conversation_list_item.view.* import model.Conversation import javax.inject.Inject class ConversationsAdapter @Inject constructor( private val context: Context, + private val colors: Colors, private val dateFormatter: DateFormatter, - private val colors: Colors + private val navigator: Navigator ) : FlowableAdapter() { - val clicks: Subject = PublishSubject.create() - val longClicks: Subject = PublishSubject.create() + private val disposables = CompositeDisposable() init { setHasStableIds(true) @@ -52,6 +54,9 @@ class ConversationsAdapter @Inject constructor( val layoutInflater = LayoutInflater.from(context) val view = layoutInflater.inflate(R.layout.conversation_list_item, parent, false) + disposables += colors.ripple + .subscribe { res -> view.setBackgroundResource(res) } + if (viewType == 1) { view.title.setTypeface(view.title.typeface, Typeface.BOLD) @@ -70,18 +75,33 @@ class ConversationsAdapter @Inject constructor( val conversation = getItem(position) val view = viewHolder.itemView - view.clicks().subscribe { clicks.onNext(conversation.id) } - view.longClicks().subscribe { longClicks.onNext(conversation.id) } + view.clicks().subscribe { + when (toggleSelection(conversation.id, false)) { + true -> view.isSelected = isSelected(conversation.id) + false -> navigator.showConversation(conversation.id) + } + } + view.longClicks().subscribe { + toggleSelection(conversation.id) + view.isSelected = isSelected(conversation.id) + } + + view.isSelected = isSelected(conversation.id) view.avatars.contacts = conversation.recipients view.title.text = conversation.getTitle() view.date.text = dateFormatter.getConversationTimestamp(conversation.date) - view.snippet.text = when(conversation.me) { + view.snippet.text = when (conversation.me) { true -> context.getString(R.string.main_sender_you, conversation.snippet) false -> conversation.snippet } } + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + disposables.clear() + } + override fun getItemId(index: Int): Long { return getItem(index).id } diff --git a/presentation/src/main/java/feature/main/MainActivity.kt b/presentation/src/main/java/feature/main/MainActivity.kt index 3797dba6..9d60a2b6 100644 --- a/presentation/src/main/java/feature/main/MainActivity.kt +++ b/presentation/src/main/java/feature/main/MainActivity.kt @@ -30,6 +30,7 @@ import android.support.v7.widget.helper.ItemTouchHelper import android.view.Gravity import android.view.Menu import android.view.MenuItem +import android.view.View import com.jakewharton.rxbinding2.support.v4.widget.drawerOpen import com.jakewharton.rxbinding2.view.clicks import com.jakewharton.rxbinding2.widget.textChanges @@ -37,14 +38,12 @@ import com.moez.QKSMS.R import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.kotlin.autoDisposable import common.Navigator -import common.QkDialog import common.base.QkThemedActivity import common.util.extensions.autoScrollToStart import common.util.extensions.dismissKeyboard import common.util.extensions.setBackgroundTint import common.util.extensions.setTint import common.util.extensions.setVisible -import common.widget.QkEditText import feature.conversations.ConversationItemTouchCallback import feature.conversations.ConversationsAdapter import injection.appComponent @@ -60,18 +59,17 @@ class MainActivity : QkThemedActivity(), MainView { @Inject lateinit var navigator: Navigator @Inject lateinit var conversationsAdapter: ConversationsAdapter - @Inject lateinit var dialog: QkDialog @Inject lateinit var itemTouchCallback: ConversationItemTouchCallback override val viewModelClass = MainViewModel::class override val queryChangedIntent by lazy { toolbarSearch.textChanges() } - override val queryCancelledIntent: PublishSubject = PublishSubject.create() override val composeIntent by lazy { compose.clicks() } override val drawerOpenIntent: Observable by lazy { drawerLayout .drawerOpen(Gravity.START) .doOnNext { dismissKeyboard() } } + override val homeIntent: Subject = PublishSubject.create() override val drawerItemIntent: Observable by lazy { Observable.merge(listOf( inbox.clicks().map { DrawerItem.INBOX }, @@ -81,15 +79,16 @@ class MainActivity : QkThemedActivity(), MainView { plus.clicks().map { DrawerItem.PLUS }, help.clicks().map { DrawerItem.HELP })) } + override val optionsItemIntent: Subject = PublishSubject.create() override val dismissRatingIntent by lazy { rateDismiss.clicks() } override val rateIntent by lazy { rateOkay.clicks() } - override val conversationClickIntent by lazy { conversationsAdapter.clicks } - override val conversationLongClickIntent by lazy { conversationsAdapter.longClicks } - override val conversationMenuItemIntent by lazy { dialog.adapter.menuItemClicks } + override val conversationsSelectedIntent by lazy { conversationsAdapter.selectionChanges } override val confirmDeleteIntent: Subject = PublishSubject.create() override val swipeConversationIntent by lazy { itemTouchCallback.swipes } override val undoSwipeConversationIntent: Subject = PublishSubject.create() + override val backPressedIntent: Subject = PublishSubject.create() + private val toggle by lazy { ActionBarDrawerToggle(this, drawerLayout, toolbar, 0, 0) } private val itemTouchHelper by lazy { ItemTouchHelper(itemTouchCallback) } private val archiveSnackbar by lazy { Snackbar.make(drawerLayout, R.string.toast_archived, Snackbar.LENGTH_INDEFINITE).apply { @@ -105,9 +104,12 @@ class MainActivity : QkThemedActivity(), MainView { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) viewModel.bindView(this) - toolbarSearch.setHint(R.string.title_conversations) - val toggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, 0, 0).apply { syncState() } + toggle.syncState() + toolbar.setNavigationOnClickListener { + dismissKeyboard() + homeIntent.onNext(Unit) + } recyclerView.setHasFixedSize(true) recyclerView.layoutManager = LinearLayoutManager(this) @@ -184,20 +186,35 @@ class MainActivity : QkThemedActivity(), MainView { } override fun render(state: MainState) { - toolbarSearch.isEnabled = state.page is Inbox - toolbarSearch.setTextSize(if (state.page is Inbox) QkEditText.SIZE_PRIMARY else QkEditText.SIZE_TOOLBAR) - - toolbar.menu.findItem(R.id.clear)?.run { - isVisible = state.page is Inbox && state.page.showClearButton + if (state.hasError) { + finish() + return } + val selectedConversations = when (state.page) { + is Inbox -> state.page.selected + is Archived -> state.page.selected + else -> 0 + } + + toolbarSearch.setVisible(state.page is Inbox && state.page.selected == 0) + toolbarTitle.setVisible(toolbarSearch.visibility != View.VISIBLE) + + toolbar.menu.findItem(R.id.archive)?.isVisible = state.page is Inbox && selectedConversations != 0 + toolbar.menu.findItem(R.id.unarchive)?.isVisible = state.page is Archived && selectedConversations != 0 + toolbar.menu.findItem(R.id.block)?.isVisible = selectedConversations != 0 + toolbar.menu.findItem(R.id.delete)?.isVisible = selectedConversations != 0 + syncing.setVisible(state.syncing) synced.setVisible(!state.syncing) rateLayout.setVisible(state.showRating) + compose.setVisible(state.page is Inbox || state.page is Archived) + when (state.page) { is Inbox -> { - if (!inbox.isSelected) toolbarSearch.text = null + showBackButton(state.page.showClearButton) + title = getString(R.string.main_title_selected, state.page.selected) if (recyclerView.adapter !== conversationsAdapter) recyclerView.adapter = conversationsAdapter conversationsAdapter.flowable = state.page.data itemTouchHelper.attachToRecyclerView(recyclerView) @@ -205,24 +222,25 @@ class MainActivity : QkThemedActivity(), MainView { true -> R.string.inbox_search_empty_text false -> R.string.inbox_empty_text }) - compose.setVisible(true) } is Archived -> { - if (!archived.isSelected) toolbarSearch.setText(R.string.title_archived) + showBackButton(state.page.showClearButton) + title = when (state.page.selected != 0) { + true -> getString(R.string.main_title_selected, state.page.selected) + false -> getString(R.string.title_archived) + } if (recyclerView.adapter !== conversationsAdapter) recyclerView.adapter = conversationsAdapter conversationsAdapter.flowable = state.page.data itemTouchHelper.attachToRecyclerView(null) empty.setText(R.string.archived_empty_text) - compose.setVisible(true) } is Scheduled -> { - if (!scheduled.isSelected) toolbarSearch.setText(R.string.title_scheduled) + setTitle(R.string.title_scheduled) recyclerView.adapter = null itemTouchHelper.attachToRecyclerView(null) empty.setText(R.string.scheduled_empty_text) - compose.setVisible(false) } } @@ -232,24 +250,24 @@ class MainActivity : QkThemedActivity(), MainView { } inbox.isSelected = state.page is Inbox - inboxIcon.isSelected = state.page is Inbox archived.isSelected = state.page is Archived - archivedIcon.isSelected = state.page is Archived scheduled.isSelected = state.page is Scheduled - scheduledIcon.isSelected = state.page is Scheduled if (drawerLayout.isDrawerOpen(Gravity.START) && !state.drawerOpen) drawerLayout.closeDrawer(Gravity.START) else if (!drawerLayout.isDrawerVisible(Gravity.START) && state.drawerOpen) drawerLayout.openDrawer(Gravity.START) } + override fun showBackButton(show: Boolean) { + toggle.onDrawerSlide(drawer, if (show) 1f else 0f) + } + override fun clearSearch() { dismissKeyboard() toolbarSearch.text = null } - override fun showDialog(menuItems: List) { - dialog.adapter.data = menuItems - dialog.show(this) + override fun clearSelection() { + conversationsAdapter.clearSelection() } override fun showDeleteDialog() { @@ -267,12 +285,12 @@ class MainActivity : QkThemedActivity(), MainView { } override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.clear -> queryCancelledIntent.onNext(Unit) - else -> return super.onOptionsItemSelected(item) - } - + optionsItemIntent.onNext(item.itemId) return true } + override fun onBackPressed() { + backPressedIntent.onNext(Unit) + } + } diff --git a/presentation/src/main/java/feature/main/MainState.kt b/presentation/src/main/java/feature/main/MainState.kt index f89e4492..fc8a755e 100644 --- a/presentation/src/main/java/feature/main/MainState.kt +++ b/presentation/src/main/java/feature/main/MainState.kt @@ -22,6 +22,7 @@ import io.reactivex.Flowable import model.Conversation data class MainState( + val hasError: Boolean = false, val page: MainPage = Inbox(), val drawerOpen: Boolean = false, val syncing: Boolean = false, @@ -33,10 +34,13 @@ sealed class MainPage data class Inbox( val showClearButton: Boolean = false, val data: Flowable>? = null, + val selected: Int = 0, val showArchivedSnackbar: Boolean = false) : MainPage() data class Archived( - val data: Flowable>? = null) : MainPage() + val showClearButton: Boolean = false, + val data: Flowable>? = null, + val selected: Int = 0) : MainPage() data class Scheduled( val data: Any? = null) : MainPage() \ No newline at end of file diff --git a/presentation/src/main/java/feature/main/MainView.kt b/presentation/src/main/java/feature/main/MainView.kt index 7237ef06..957c055f 100644 --- a/presentation/src/main/java/feature/main/MainView.kt +++ b/presentation/src/main/java/feature/main/MainView.kt @@ -18,28 +18,27 @@ */ package feature.main -import common.MenuItem import common.base.QkView import io.reactivex.Observable interface MainView : QkView { val queryChangedIntent: Observable - val queryCancelledIntent: Observable<*> val composeIntent: Observable val drawerOpenIntent: Observable + val homeIntent: Observable<*> val drawerItemIntent: Observable + val optionsItemIntent: Observable val dismissRatingIntent: Observable<*> val rateIntent: Observable<*> - val conversationClickIntent: Observable - val conversationLongClickIntent: Observable - val conversationMenuItemIntent: Observable + val conversationsSelectedIntent: Observable> val confirmDeleteIntent: Observable val swipeConversationIntent: Observable val undoSwipeConversationIntent: Observable + val backPressedIntent: Observable fun clearSearch() - fun showDialog(menuItems: List) + fun clearSelection() fun showDeleteDialog() } diff --git a/presentation/src/main/java/feature/main/MainViewModel.kt b/presentation/src/main/java/feature/main/MainViewModel.kt index 67429870..a291ae1a 100644 --- a/presentation/src/main/java/feature/main/MainViewModel.kt +++ b/presentation/src/main/java/feature/main/MainViewModel.kt @@ -26,12 +26,11 @@ import android.support.v4.content.ContextCompat import com.moez.QKSMS.R import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.kotlin.autoDisposable -import common.MenuItem import common.Navigator import common.base.QkViewModel import common.util.filter.ConversationFilter import injection.appComponent -import interactor.DeleteConversation +import interactor.DeleteConversations import interactor.MarkAllSeen import interactor.MarkArchived import interactor.MarkBlocked @@ -56,7 +55,7 @@ class MainViewModel : QkViewModel(MainState()) { @Inject lateinit var conversationFilter: ConversationFilter @Inject lateinit var messageRepo: MessageRepository @Inject lateinit var markAllSeen: MarkAllSeen - @Inject lateinit var deleteConversation: DeleteConversation + @Inject lateinit var deleteConversations: DeleteConversations @Inject lateinit var markArchived: MarkArchived @Inject lateinit var markUnarchived: MarkUnarchived @Inject lateinit var markBlocked: MarkBlocked @@ -66,11 +65,6 @@ class MainViewModel : QkViewModel(MainState()) { @Inject lateinit var prefs: Preferences @Inject lateinit var ratingManager: RatingManager - private val menuArchive by lazy { MenuItem(context.getString(R.string.inbox_menu_archive), 0) } - private val menuUnarchive by lazy { MenuItem(context.getString(R.string.inbox_menu_unarchive), 1) } - private val menuBlock by lazy { MenuItem(context.getString(R.string.inbox_menu_block), 2) } - private val menuDelete by lazy { MenuItem(context.getString(R.string.inbox_menu_delete), 3) } - private val conversations by lazy { messageRepo.getConversations() } init { @@ -79,7 +73,7 @@ class MainViewModel : QkViewModel(MainState()) { // Now the conversations can be loaded conversations - disposables += deleteConversation + disposables += deleteConversations disposables += markAllSeen disposables += markArchived disposables += markUnarchived @@ -138,14 +132,24 @@ class MainViewModel : QkViewModel(MainState()) { .autoDisposable(view.scope()) .subscribe() - view.queryCancelledIntent - .autoDisposable(view.scope()) - .subscribe { view.clearSearch() } - view.composeIntent .autoDisposable(view.scope()) .subscribe { navigator.showCompose() } + view.homeIntent + .withLatestFrom(state, { _, state -> + when { + state.page is Inbox && state.page.selected > 0 -> view.clearSelection() + state.page is Inbox && state.page.showClearButton -> view.clearSearch() + + state.page is Archived && state.page.selected > 0 -> view.clearSelection() + + else -> newState { it.copy(drawerOpen = true) } + } + }) + .autoDisposable(view.scope()) + .subscribe() + view.drawerOpenIntent .autoDisposable(view.scope()) .subscribe { open -> newState { it.copy(drawerOpen = open) } } @@ -158,8 +162,8 @@ class MainViewModel : QkViewModel(MainState()) { .distinctUntilChanged() .doOnNext { when (it) { - DrawerItem.INBOX -> newState { it.copy(page = Inbox()) } - DrawerItem.ARCHIVED -> newState { it.copy(page = Archived(messageRepo.getConversations(true))) } + DrawerItem.INBOX -> newState { it.copy(page = Inbox(data = conversations)) } + DrawerItem.ARCHIVED -> newState { it.copy(page = Archived(data = messageRepo.getConversations(true))) } DrawerItem.SCHEDULED -> newState { it.copy(page = Scheduled()) } else -> { } // Do nothing @@ -168,6 +172,30 @@ class MainViewModel : QkViewModel(MainState()) { .autoDisposable(view.scope()) .subscribe() + view.optionsItemIntent + .withLatestFrom(view.conversationsSelectedIntent, { itemId, conversations -> + when (itemId) { + R.id.archive -> { + markArchived.execute(conversations) + view.clearSelection() + } + + R.id.unarchive -> { + markUnarchived.execute(conversations) + view.clearSelection() + } + + R.id.block -> { + markBlocked.execute(conversations) + view.clearSelection() + } + + R.id.delete -> view.showDeleteDialog() + } + }) + .autoDisposable(view.scope()) + .subscribe() + view.rateIntent .autoDisposable(view.scope()) .subscribe { @@ -179,29 +207,19 @@ class MainViewModel : QkViewModel(MainState()) { .autoDisposable(view.scope()) .subscribe { ratingManager.dismiss() } - view.conversationClickIntent - .doOnNext { view.clearSearch() } - .doOnNext { threadId -> navigator.showConversation(threadId) } - .autoDisposable(view.scope()) - .subscribe() + view.conversationsSelectedIntent + .map { selection -> selection.size } + .withLatestFrom(state, { selected, state -> + when (state.page) { + is Inbox -> { + val page = state.page.copy(selected = selected, showClearButton = selected > 0) + newState { it.copy(page = page) } + } - view.conversationLongClickIntent - .withLatestFrom(state, { _, mainState -> - when (mainState.page) { - is Inbox -> view.showDialog(listOf(menuArchive, menuBlock, menuDelete)) - is Archived -> view.showDialog(listOf(menuUnarchive, menuBlock, menuDelete)) - } - }) - .autoDisposable(view.scope()) - .subscribe() - - view.conversationMenuItemIntent - .withLatestFrom(view.conversationLongClickIntent, { actionId, threadId -> - when (actionId) { - menuArchive.actionId -> markArchived.execute(threadId) - menuUnarchive.actionId -> markUnarchived.execute(threadId) - menuBlock.actionId -> markBlocked.execute(threadId) - menuDelete.actionId -> view.showDeleteDialog() + is Archived -> { + val page = state.page.copy(selected = selected, showClearButton = selected > 0) + newState { it.copy(page = page) } + } } }) .autoDisposable(view.scope()) @@ -209,13 +227,16 @@ class MainViewModel : QkViewModel(MainState()) { // Delete the conversation view.confirmDeleteIntent - .withLatestFrom(view.conversationLongClickIntent, { _, threadId -> threadId }) + .withLatestFrom(view.conversationsSelectedIntent, { _, conversations -> conversations }) .autoDisposable(view.scope()) - .subscribe { threadId -> deleteConversation.execute(threadId) } + .subscribe { conversations -> + deleteConversations.execute(conversations) + view.clearSelection() + } view.swipeConversationIntent .withLatestFrom(state, { threadId, state -> - markArchived.execute(threadId) { + markArchived.execute(listOf(threadId)) { if (state.page is Inbox) { val page = state.page.copy(showArchivedSnackbar = true) newState { it.copy(page = page) } @@ -224,7 +245,7 @@ class MainViewModel : QkViewModel(MainState()) { }) .switchMap { Observable.timer(2750, TimeUnit.MILLISECONDS) } .withLatestFrom(state, { threadId, state -> - markArchived.execute(threadId) { + markArchived.execute(listOf(threadId)) { if (state.page is Inbox) { val page = state.page.copy(showArchivedSnackbar = false) newState { it.copy(page = page) } @@ -237,7 +258,25 @@ class MainViewModel : QkViewModel(MainState()) { view.undoSwipeConversationIntent .withLatestFrom(view.swipeConversationIntent, { _, threadId -> threadId }) .autoDisposable(view.scope()) - .subscribe { threadId -> markUnarchived.execute(threadId) } + .subscribe { threadId -> markUnarchived.execute(listOf(threadId)) } + + view.backPressedIntent + .withLatestFrom(state, { _, state -> + when { + state.drawerOpen -> newState { it.copy(drawerOpen = false) } + + state.page is Inbox && state.page.selected > 0 -> view.clearSelection() + state.page is Inbox && state.page.showClearButton -> view.clearSearch() + + state.page is Archived && state.page.selected > 0 -> view.clearSelection() + + state.page !is Inbox -> newState { it.copy(page = Inbox(data = conversations)) } + + else -> newState { it.copy(hasError = true) } + } + }) + .autoDisposable(view.scope()) + .subscribe() } } \ No newline at end of file diff --git a/presentation/src/main/java/interactor/SendMessage.kt b/presentation/src/main/java/interactor/SendMessage.kt index fc59be32..83d51a67 100644 --- a/presentation/src/main/java/interactor/SendMessage.kt +++ b/presentation/src/main/java/interactor/SendMessage.kt @@ -50,7 +50,7 @@ class SendMessage @Inject constructor( sendMms(params.threadId, params.addresses, params.body, params.attachments) } } - .doOnNext { messageRepo.updateConversation(params.threadId) } + .doOnNext { messageRepo.updateConversations(params.threadId) } .doOnNext { messageRepo.markUnarchived(params.threadId) } } diff --git a/presentation/src/main/res/drawable/ripple.xml b/presentation/src/main/res/drawable/ripple.xml index eef2532d..a4ef9eba 100644 --- a/presentation/src/main/res/drawable/ripple.xml +++ b/presentation/src/main/res/drawable/ripple.xml @@ -18,7 +18,7 @@ ~ along with QKSMS. If not, see . --> + android:color="@color/rippleLight"> @@ -30,10 +30,7 @@ - - - - + diff --git a/presentation/src/main/res/drawable/ripple_dark.xml b/presentation/src/main/res/drawable/ripple_dark.xml new file mode 100644 index 00000000..84803c38 --- /dev/null +++ b/presentation/src/main/res/drawable/ripple_dark.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/main_activity.xml b/presentation/src/main/res/layout/main_activity.xml index 6662d193..99bd5206 100644 --- a/presentation/src/main/res/layout/main_activity.xml +++ b/presentation/src/main/res/layout/main_activity.xml @@ -32,6 +32,7 @@ + + diff --git a/presentation/src/main/res/menu/main.xml b/presentation/src/main/res/menu/main.xml index 8ceda5f9..51004a72 100644 --- a/presentation/src/main/res/menu/main.xml +++ b/presentation/src/main/res/menu/main.xml @@ -21,10 +21,31 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + app:showAsAction="ifRoom" /> + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/values/colors.xml b/presentation/src/main/res/values/colors.xml index 00988559..5e5fd929 100644 --- a/presentation/src/main/res/values/colors.xml +++ b/presentation/src/main/res/values/colors.xml @@ -21,6 +21,9 @@ #FFFFFF #000000 + #1f000000 + #33ffffff + #F7F7F7 #1a1e22 diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index e9824303..26c3b841 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -34,13 +34,22 @@ Skip Continue - Clear Call Details Save image + %d selected + Clear + Archive + Unarchive + Block + Delete Optimizing database, this might take a minute… You: %s + Your conversations will appear here + No results + Your archived conversations will appear here + Your scheduled messages will appear here Inbox Archived @@ -55,15 +64,6 @@ OKAY! DISMISS - Your conversations will appear here - No results - Archive - Unarchive - Block - Delete - Your archived conversations will appear here - Your scheduled messages will appear here - Delete Are you sure you would like to delete this conversation?