From cb62bcee1dc03f19788c9c13430ee9d1d78e8efe Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 23 Jul 2018 14:37:42 -0400 Subject: [PATCH] #965 - Add ability to pin conversations --- .../moez/QKSMS/migration/QkRealmMigration.kt | 9 +++- .../repository/ConversationRepositoryImpl.kt | 28 +++++++++- .../com/moez/QKSMS/interactor/MarkPinned.kt | 32 ++++++++++++ .../com/moez/QKSMS/interactor/MarkUnpinned.kt | 32 ++++++++++++ .../java/com/moez/QKSMS/model/Conversation.kt | 1 + .../repository/ConversationRepository.kt | 4 ++ .../ConversationInfoViewModel.kt | 8 +-- .../conversations/ConversationsAdapter.kt | 2 + .../moez/QKSMS/feature/main/MainActivity.kt | 14 +++-- .../com/moez/QKSMS/feature/main/MainState.kt | 2 + .../moez/QKSMS/feature/main/MainViewModel.kt | 51 ++++++++++++------- .../main/res/drawable/ic_pin_black_24dp.xml | 27 ++++++++++ .../res/layout/conversation_list_item.xml | 14 ++++- presentation/src/main/res/menu/main.xml | 14 +++++ presentation/src/main/res/values/strings.xml | 8 +-- 15 files changed, 215 insertions(+), 31 deletions(-) create mode 100644 domain/src/main/java/com/moez/QKSMS/interactor/MarkPinned.kt create mode 100644 domain/src/main/java/com/moez/QKSMS/interactor/MarkUnpinned.kt create mode 100644 presentation/src/main/res/drawable/ic_pin_black_24dp.xml diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index a363236d..a8d5a38f 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -26,7 +26,7 @@ import io.realm.RealmMigration class QkRealmMigration : RealmMigration { companion object { - const val SCHEMA_VERSION: Long = 4 + const val SCHEMA_VERSION: Long = 5 } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -66,6 +66,13 @@ class QkRealmMigration : RealmMigration { version++ } + if (version == 4L) { + realm.schema.get("Conversation") + ?.addField("pinned", Boolean::class.java, FieldAttribute.REQUIRED, FieldAttribute.INDEXED) + + version++ + } + if (version < newVersion) { throw IllegalStateException("Migration missing from v$oldVersion to v$newVersion") } diff --git a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt index 318df699..e8d31da8 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt @@ -36,7 +36,7 @@ class ConversationRepositoryImpl @Inject constructor( .equalTo("archived", archived) .equalTo("blocked", false) .isNotEmpty("recipients") - .sort("date", Sort.DESCENDING) + .sort("pinned", Sort.DESCENDING, "date", Sort.DESCENDING) .findAllAsync() } @@ -48,7 +48,7 @@ class ConversationRepositoryImpl @Inject constructor( .equalTo("archived", false) .equalTo("blocked", false) .isNotEmpty("recipients") - .sort("date", Sort.DESCENDING) + .sort("pinned", Sort.DESCENDING, "date", Sort.DESCENDING) .findAll()) } @@ -206,6 +206,30 @@ class ConversationRepositoryImpl @Inject constructor( } } + override fun markPinned(vararg threadIds: Long) { + Realm.getDefaultInstance().use { realm -> + val conversations = realm.where(Conversation::class.java) + .anyOf("id", threadIds) + .findAll() + + realm.executeTransaction { + conversations.forEach { it.pinned = true } + } + } + } + + override fun markUnpinned(vararg threadIds: Long) { + Realm.getDefaultInstance().use { realm -> + val conversations = realm.where(Conversation::class.java) + .anyOf("id", threadIds) + .findAll() + + realm.executeTransaction { + conversations.forEach { it.pinned = false } + } + } + } + override fun markBlocked(vararg threadIds: Long) { Realm.getDefaultInstance().use { realm -> val conversations = realm.where(Conversation::class.java) diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/MarkPinned.kt b/domain/src/main/java/com/moez/QKSMS/interactor/MarkPinned.kt new file mode 100644 index 00000000..c5093616 --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/interactor/MarkPinned.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.interactor + +import com.moez.QKSMS.repository.ConversationRepository +import io.reactivex.Flowable +import javax.inject.Inject + +class MarkPinned @Inject constructor(private val conversationRepo: ConversationRepository) : Interactor>() { + + override fun buildObservable(params: List): Flowable<*> { + return Flowable.just(params.toLongArray()) + .doOnNext { threadIds -> conversationRepo.markPinned(*threadIds) } + } + +} \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/MarkUnpinned.kt b/domain/src/main/java/com/moez/QKSMS/interactor/MarkUnpinned.kt new file mode 100644 index 00000000..e8501495 --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/interactor/MarkUnpinned.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.interactor + +import com.moez.QKSMS.repository.ConversationRepository +import io.reactivex.Flowable +import javax.inject.Inject + +class MarkUnpinned @Inject constructor(private val conversationRepo: ConversationRepository) : Interactor>() { + + override fun buildObservable(params: List): Flowable<*> { + return Flowable.just(params.toLongArray()) + .doOnNext { threadIds -> conversationRepo.markUnpinned(*threadIds) } + } + +} \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/model/Conversation.kt b/domain/src/main/java/com/moez/QKSMS/model/Conversation.kt index f90f1278..c42af5e7 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/Conversation.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/Conversation.kt @@ -27,6 +27,7 @@ open class Conversation( @PrimaryKey var id: Long = 0, @Index var archived: Boolean = false, @Index var blocked: Boolean = false, + @Index var pinned: Boolean = false, var recipients: RealmList = RealmList(), var count: Int = 0, var date: Long = 0, diff --git a/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt index 0eddb458..ada8c4a4 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt @@ -41,6 +41,10 @@ interface ConversationRepository { fun markUnarchived(vararg threadIds: Long) + fun markPinned(vararg threadIds: Long) + + fun markUnpinned(vararg threadIds: Long) + fun markBlocked(vararg threadIds: Long) fun markUnblocked(vararg threadIds: Long) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoViewModel.kt index a80860ca..7ad9e86c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoViewModel.kt @@ -40,14 +40,14 @@ import javax.inject.Named class ConversationInfoViewModel @Inject constructor( @Named("threadId") threadId: Long, + messageRepo: MessageRepository, private val conversationRepo: ConversationRepository, + private val deleteConversations: DeleteConversations, private val markArchived: MarkArchived, private val markUnarchived: MarkUnarchived, private val markBlocked: MarkBlocked, private val markUnblocked: MarkUnblocked, - private val messageRepo: MessageRepository, - private val navigator: Navigator, - private val deleteConversations: DeleteConversations + private val navigator: Navigator ) : QkViewModel( ConversationInfoState(threadId = threadId, media = messageRepo.getPartsForConversation(threadId)) ) { @@ -56,7 +56,7 @@ class ConversationInfoViewModel @Inject constructor( init { disposables += conversationRepo.getConversationAsync(threadId) - .asObservable() + .asObservable() .filter { conversation -> conversation.isLoaded } .doOnNext { conversation -> if (!conversation.isValid) { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt index dcc0e3a0..de06fb90 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationsAdapter.kt @@ -22,6 +22,7 @@ import android.content.Context import android.graphics.Typeface import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.view.isVisible import com.moez.QKSMS.R import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.base.QkRealmAdapter @@ -87,6 +88,7 @@ class ConversationsAdapter @Inject constructor( true -> context.getString(R.string.main_sender_you, conversation.snippet) false -> conversation.snippet } + view.pinned.isVisible = conversation.pinned } override fun getItemId(index: Int): Long { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt index ec50f04d..4ada050e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt @@ -166,6 +166,12 @@ class MainActivity : QkThemedActivity(), MainView { return } + val markPinned = when (state.page) { + is Inbox -> state.page.markPinned + is Archived -> state.page.markPinned + else -> true + } + val markRead = when (state.page) { is Inbox -> state.page.markRead is Archived -> state.page.markRead @@ -181,12 +187,14 @@ class MainActivity : QkThemedActivity(), MainView { toolbarSearch.setVisible(state.page is Inbox && state.page.selected == 0 || state.page is Searching) toolbarTitle.setVisible(toolbarSearch.visibility != View.VISIBLE) - toolbar.menu.findItem(R.id.read)?.isVisible = markRead && selectedConversations != 0 - toolbar.menu.findItem(R.id.unread)?.isVisible = !markRead && selectedConversations != 0 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 + toolbar.menu.findItem(R.id.pin)?.isVisible = markPinned && selectedConversations != 0 + toolbar.menu.findItem(R.id.unpin)?.isVisible = !markPinned && selectedConversations != 0 + toolbar.menu.findItem(R.id.read)?.isVisible = markRead && selectedConversations != 0 + toolbar.menu.findItem(R.id.unread)?.isVisible = !markRead && selectedConversations != 0 + toolbar.menu.findItem(R.id.block)?.isVisible = selectedConversations != 0 plus.isVisible = state.upgraded plusBanner.isVisible = !state.upgraded diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt index b40ba895..85ced652 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt @@ -39,6 +39,7 @@ sealed class MainPage data class Inbox( val showClearButton: Boolean = false, + val markPinned: Boolean = true, val markRead: Boolean = false, val data: RealmResults? = null, val selected: Int = 0, @@ -51,6 +52,7 @@ data class Searching( data class Archived( val showClearButton: Boolean = false, + val markPinned: Boolean = true, val markRead: Boolean = false, val data: RealmResults? = null, val selected: Int = 0) : MainPage() \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt index 5e378019..3106c01c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt @@ -28,8 +28,10 @@ import com.moez.QKSMS.interactor.DeleteConversations import com.moez.QKSMS.interactor.MarkAllSeen import com.moez.QKSMS.interactor.MarkArchived import com.moez.QKSMS.interactor.MarkBlocked +import com.moez.QKSMS.interactor.MarkPinned import com.moez.QKSMS.interactor.MarkRead import com.moez.QKSMS.interactor.MarkUnarchived +import com.moez.QKSMS.interactor.MarkUnpinned import com.moez.QKSMS.interactor.MarkUnread import com.moez.QKSMS.interactor.MigratePreferences import com.moez.QKSMS.interactor.SyncMessages @@ -56,11 +58,13 @@ class MainViewModel @Inject constructor( syncRepository: SyncRepository, private val conversationRepo: ConversationRepository, private val deleteConversations: DeleteConversations, - private val markRead: MarkRead, - private val markUnread: MarkUnread, private val markArchived: MarkArchived, - private val markUnarchived: MarkUnarchived, private val markBlocked: MarkBlocked, + private val markPinned: MarkPinned, + private val markRead: MarkRead, + private val markUnarchived: MarkUnarchived, + private val markUnpinned: MarkUnpinned, + private val markUnread: MarkUnread, private val navigator: Navigator, private val permissionManager: PermissionManager, private val ratingManager: RatingManager, @@ -205,16 +209,6 @@ class MainViewModel @Inject constructor( view.optionsItemIntent .withLatestFrom(view.conversationsSelectedIntent) { itemId, conversations -> when (itemId) { - R.id.read -> { - markRead.execute(conversations) - view.clearSelection() - } - - R.id.unread -> { - markUnread.execute(conversations) - view.clearSelection() - } - R.id.archive -> { markArchived.execute(conversations) view.clearSelection() @@ -225,12 +219,32 @@ class MainViewModel @Inject constructor( view.clearSelection() } + R.id.delete -> view.showDeleteDialog() + + R.id.pin -> { + markPinned.execute(conversations) + view.clearSelection() + } + + R.id.unpin -> { + markUnpinned.execute(conversations) + view.clearSelection() + } + + R.id.read -> { + markRead.execute(conversations) + view.clearSelection() + } + + R.id.unread -> { + markUnread.execute(conversations) + view.clearSelection() + } + R.id.block -> { markBlocked.execute(conversations) view.clearSelection() } - - R.id.delete -> view.showDeleteDialog() } } .autoDisposable(view.scope()) @@ -256,6 +270,9 @@ class MainViewModel @Inject constructor( view.conversationsSelectedIntent .withLatestFrom(state) { selection, state -> + val pin = selection + .mapNotNull(conversationRepo::getConversation) + .sumBy { if (it.pinned) -1 else 1 } >= 0 val read = selection .mapNotNull(conversationRepo::getConversation) .sumBy { if (it.read) -1 else 1 } >= 0 @@ -263,12 +280,12 @@ class MainViewModel @Inject constructor( when (state.page) { is Inbox -> { - val page = state.page.copy(markRead = read, selected = selected, showClearButton = selected > 0) + val page = state.page.copy(markPinned = pin, markRead = read, selected = selected, showClearButton = selected > 0) newState { copy(page = page.copy(markRead = read, selected = selected, showClearButton = selected > 0)) } } is Archived -> { - val page = state.page.copy(markRead = read, selected = selected, showClearButton = selected > 0) + val page = state.page.copy(markPinned = pin, markRead = read, selected = selected, showClearButton = selected > 0) newState { copy(page = page) } } } diff --git a/presentation/src/main/res/drawable/ic_pin_black_24dp.xml b/presentation/src/main/res/drawable/ic_pin_black_24dp.xml new file mode 100644 index 00000000..838ba274 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_pin_black_24dp.xml @@ -0,0 +1,27 @@ + + + + diff --git a/presentation/src/main/res/layout/conversation_list_item.xml b/presentation/src/main/res/layout/conversation_list_item.xml index 75ec2e3a..fbf423c8 100644 --- a/presentation/src/main/res/layout/conversation_list_item.xml +++ b/presentation/src/main/res/layout/conversation_list_item.xml @@ -63,11 +63,23 @@ android:ellipsize="end" android:maxLines="1" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/pinned" app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title" tools:text="@tools:sample/lorem/random" /> + + + + + + Open navigation drawer %d selected Clear - Mark read - Mark unread Archive Unarchive - Block Delete + Pin to top + Unpin + Mark read + Mark unread + Block Syncing messages… You: %s Results in messages