From 75162c8114ba8d41d37f8322088ae757fa4e79c6 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Mon, 30 Jul 2018 20:34:31 -0400 Subject: [PATCH] #967 - Swipe conversation actions --- .../java/com/moez/QKSMS/util/Preferences.kt | 22 +- .../ConversationItemTouchCallback.kt | 91 ++++-- .../moez/QKSMS/feature/main/MainActivity.kt | 29 +- .../QKSMS/feature/main/MainActivityModule.kt | 4 + .../com/moez/QKSMS/feature/main/MainState.kt | 3 +- .../com/moez/QKSMS/feature/main/MainView.kt | 5 +- .../moez/QKSMS/feature/main/MainViewModel.kt | 39 +-- .../feature/settings/SettingsController.kt | 15 +- .../feature/settings/SettingsPresenter.kt | 2 + .../QKSMS/feature/settings/SettingsView.kt | 1 + .../settings/swipe/SwipeActionsController.kt | 99 ++++++ .../settings/swipe/SwipeActionsPresenter.kt | 79 +++++ .../settings/swipe/SwipeActionsState.kt | 30 ++ .../settings/swipe/SwipeActionsView.kt | 33 ++ .../com/moez/QKSMS/injection/AppComponent.kt | 2 + .../res/drawable/ic_clear_all_black_24dp.xml | 27 ++ .../drawable/rounded_rectangle_left_4dp.xml | 29 ++ .../rounded_rectangle_outline_4dp.xml | 30 ++ .../drawable/rounded_rectangle_right_4dp.xml | 29 ++ ...s_activity.xml => settings_controller.xml} | 9 + .../res/layout/swipe_actions_controller.xml | 308 ++++++++++++++++++ presentation/src/main/res/values/strings.xml | 12 + 22 files changed, 820 insertions(+), 78 deletions(-) create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsController.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsPresenter.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsState.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsView.kt create mode 100644 presentation/src/main/res/drawable/ic_clear_all_black_24dp.xml create mode 100755 presentation/src/main/res/drawable/rounded_rectangle_left_4dp.xml create mode 100755 presentation/src/main/res/drawable/rounded_rectangle_outline_4dp.xml create mode 100755 presentation/src/main/res/drawable/rounded_rectangle_right_4dp.xml rename presentation/src/main/res/layout/{settings_activity.xml => settings_controller.xml} (95%) create mode 100644 presentation/src/main/res/layout/swipe_actions_controller.xml diff --git a/data/src/main/java/com/moez/QKSMS/util/Preferences.kt b/data/src/main/java/com/moez/QKSMS/util/Preferences.kt index fd1b0d2e..81f5380e 100644 --- a/data/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/data/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -52,6 +52,12 @@ class Preferences @Inject constructor(private val rxPrefs: RxSharedPreferences) const val SEND_DELAY_SHORT = 1 const val SEND_DELAY_MEDIUM = 2 const val SEND_DELAY_LONG = 3 + + const val SWIPE_ACTION_NONE = 0 + const val SWIPE_ACTION_ARCHIVE = 1 + const val SWIPE_ACTION_DELETE = 2 + const val SWIPE_ACTION_CALL = 3 + const val SWIPE_ACTION_READ = 4 } // Internal @@ -63,17 +69,19 @@ class Preferences @Inject constructor(private val rxPrefs: RxSharedPreferences) val nightStart = rxPrefs.getString("nightStart", "18:00") val nightEnd = rxPrefs.getString("nightEnd", "6:00") val black = rxPrefs.getBoolean("black", false) - val sia = rxPrefs.getBoolean("sia", false) - val sendDelay = rxPrefs.getInteger("sendDelay", SEND_DELAY_NONE) - val autoEmoji = rxPrefs.getBoolean("autoEmoji", true) - val delivery = rxPrefs.getBoolean("delivery", false) val systemFont = rxPrefs.getBoolean("systemFont", false) val textSize = rxPrefs.getInteger("textSize", TEXT_SIZE_NORMAL) - val notifAction1 = rxPrefs.getInteger("notifAction1", 1) - val notifAction2 = rxPrefs.getInteger("notifAction2", 2) - val notifAction3 = rxPrefs.getInteger("notifAction3", 0) + val sia = rxPrefs.getBoolean("sia", false) + val notifAction1 = rxPrefs.getInteger("notifAction1", NOTIFICATION_ACTION_READ) + val notifAction2 = rxPrefs.getInteger("notifAction2", NOTIFICATION_ACTION_REPLY) + val notifAction3 = rxPrefs.getInteger("notifAction3", NOTIFICATION_ACTION_NONE) val qkreply = rxPrefs.getBoolean("qkreply", Build.VERSION.SDK_INT < Build.VERSION_CODES.N) val qkreplyTapDismiss = rxPrefs.getBoolean("qkreplyTapDismiss", true) + val sendDelay = rxPrefs.getInteger("sendDelay", SEND_DELAY_NONE) + val swipeRight = rxPrefs.getInteger("swipeRight", SWIPE_ACTION_ARCHIVE) + val swipeLeft = rxPrefs.getInteger("swipeLeft", SWIPE_ACTION_ARCHIVE) + val autoEmoji = rxPrefs.getBoolean("autoEmoji", true) + val delivery = rxPrefs.getBoolean("delivery", false) val unicode = rxPrefs.getBoolean("unicode", false) val mmsSize = rxPrefs.getInteger("mmsSize", 300) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationItemTouchCallback.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationItemTouchCallback.kt index 5d802ee0..2c5d117e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationItemTouchCallback.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversations/ConversationItemTouchCallback.kt @@ -23,37 +23,60 @@ import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint import androidx.core.graphics.drawable.toBitmap +import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.moez.QKSMS.R +import com.moez.QKSMS.common.androidxcompat.scope +import com.moez.QKSMS.common.util.Colors import com.moez.QKSMS.common.util.extensions.dpToPx +import com.moez.QKSMS.util.Preferences +import com.uber.autodispose.kotlin.autoDisposable +import io.reactivex.rxkotlin.Observables import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import javax.inject.Inject -class ConversationItemTouchCallback @Inject constructor(private val context: Context) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { +class ConversationItemTouchCallback @Inject constructor( + colors: Colors, + lifecycle: Lifecycle, + prefs: Preferences, + private val context: Context +) : ItemTouchHelper.SimpleCallback(0, 0) { - val swipes: Subject = PublishSubject.create() + val swipes: Subject> = PublishSubject.create() - private var paint = Paint() - private lateinit var icon: Bitmap + /** + * Setting the adapter allows us to animate back to the original position + */ + var adapter: RecyclerView.Adapter<*>? = null - private var iconLength = 24.dpToPx(context) + private val paint = Paint() + private var rightAction = 0 + private var rightIcon: Bitmap? = null + private var leftAction = 0 + private var leftIcon: Bitmap? = null - var color: Int = 0 - set(value) { - field = value - paint.color = value - } + private val iconLength = 24.dpToPx(context) - var iconColor: Int = 0 - set(value) { - field = value - icon = context.resources.getDrawable(R.drawable.ic_archive_black_24dp) - .apply { setTint(value) } - .toBitmap(iconLength, iconLength) - } + init { + colors.themeObservable() + .autoDisposable(lifecycle.scope()) + .subscribe { theme -> paint.color = theme.theme } + + Observables + .combineLatest(prefs.swipeRight.asObservable(), prefs.swipeLeft.asObservable(), colors.themeObservable()) { right, left, theme -> + rightAction = right + rightIcon = iconForAction(right, theme.textPrimary) + leftAction = left + leftIcon = iconForAction(left, theme.textPrimary) + setDefaultSwipeDirs((if (right == Preferences.SWIPE_ACTION_NONE) 0 else ItemTouchHelper.RIGHT) + or (if (left == Preferences.SWIPE_ACTION_NONE) 0 else ItemTouchHelper.LEFT)) + } + .autoDisposable(lifecycle.scope()) + .subscribe() + } override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { return false @@ -65,14 +88,14 @@ class ConversationItemTouchCallback @Inject constructor(private val context: Con if (dX > 0) { c.drawRect(itemView.left.toFloat(), itemView.top.toFloat(), dX, itemView.bottom.toFloat(), paint) - c.drawBitmap(icon, itemView.left.toFloat() + iconLength, - itemView.top.toFloat() + (itemView.bottom.toFloat() - itemView.top.toFloat() - icon.height) / 2, - paint) + c.drawBitmap(rightIcon, itemView.left.toFloat() + iconLength, + itemView.top.toFloat() + (itemView.bottom.toFloat() - itemView.top.toFloat() - (rightIcon?.height + ?: 0)) / 2, paint) } else if (dX < 0) { c.drawRect(itemView.right.toFloat() + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat(), paint) - c.drawBitmap(icon, itemView.right.toFloat() - iconLength - icon.width, - itemView.top.toFloat() + (itemView.bottom.toFloat() - itemView.top.toFloat() - icon.height) / 2, - paint) + c.drawBitmap(leftIcon, itemView.right.toFloat() - iconLength - (leftIcon?.width ?: 0), + itemView.top.toFloat() + (itemView.bottom.toFloat() - itemView.top.toFloat() - (leftIcon?.height + ?: 0)) / 2, paint) } super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) @@ -80,7 +103,27 @@ class ConversationItemTouchCallback @Inject constructor(private val context: Con } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - swipes.onNext(viewHolder.itemId) + swipes.onNext(Pair(viewHolder.itemId, direction)) + + // This will trigger the animation back to neutral state + val action = if (direction == ItemTouchHelper.RIGHT) rightAction else leftAction + if (action != Preferences.SWIPE_ACTION_ARCHIVE && action != Preferences.SWIPE_ACTION_READ) { + adapter?.notifyItemChanged(viewHolder.adapterPosition) + } + } + + private fun iconForAction(action: Int, tint: Int): Bitmap? { + val res = when (action) { + Preferences.SWIPE_ACTION_ARCHIVE -> R.drawable.ic_archive_black_24dp + Preferences.SWIPE_ACTION_DELETE -> R.drawable.ic_delete_white_24dp + Preferences.SWIPE_ACTION_CALL -> R.drawable.ic_call_white_24dp + Preferences.SWIPE_ACTION_READ -> R.drawable.ic_check_white_24dp + else -> null + } + + return res?.let(context.resources::getDrawable) + ?.apply { setTint(tint) } + ?.toBitmap(iconLength, iconLength) } } \ No newline at end of file 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 a48a8813..90d6b359 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 @@ -95,7 +95,7 @@ class MainActivity : QkThemedActivity(), MainView { 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 undoArchiveIntent: Subject = PublishSubject.create() override val snackbarButtonIntent by lazy { snackbarButton.clicks() } override val backPressedIntent: Subject = PublishSubject.create() @@ -103,8 +103,8 @@ class MainActivity : QkThemedActivity(), MainView { private val toggle by lazy { ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.main_drawer_open_cd, 0) } private val itemTouchHelper by lazy { ItemTouchHelper(itemTouchCallback) } private val archiveSnackbar by lazy { - Snackbar.make(drawerLayout, R.string.toast_archived, Snackbar.LENGTH_INDEFINITE).apply { - setAction(R.string.button_undo) { undoSwipeConversationIntent.onNext(Unit) } + Snackbar.make(drawerLayout, R.string.toast_archived, Snackbar.LENGTH_LONG).apply { + setAction(R.string.button_undo) { undoArchiveIntent.onNext(Unit) } } } @@ -129,7 +129,8 @@ class MainActivity : QkThemedActivity(), MainView { // Set the theme color tint to the recyclerView, progressbar, and FAB colors.themeObservable() .doOnNext { recyclerView.scrapViews() } - .doOnNext { theme -> + .autoDisposable(scope()) + .subscribe { theme -> // Set the color for the drawer icons val states = arrayOf(intArrayOf(android.R.attr.state_selected), intArrayOf(-android.R.attr.state_selected)) resolveThemeColor(android.R.attr.textColorSecondary) @@ -138,26 +139,21 @@ class MainActivity : QkThemedActivity(), MainView { inboxIcon.imageTintList = tintList archivedIcon.imageTintList = tintList } - } - .doOnNext { theme -> + // Miscellaneous views syncingProgress.indeterminateTintList = ColorStateList.valueOf(theme.theme) - itemTouchCallback.color = theme.theme plusIcon.setTint(theme.theme) rateIcon.setTint(theme.theme) compose.setBackgroundTint(theme.theme) - } - .doOnNext { theme -> + // Set the FAB compose icon color compose.setTint(theme.textPrimary) - itemTouchCallback.iconColor = theme.textPrimary } - .autoDisposable(scope()) - .subscribe() // Set the hamburger icon color toggle.drawerArrowDrawable.color = resolveThemeColor(android.R.attr.textColorSecondary) + itemTouchCallback.adapter = conversationsAdapter conversationsAdapter.autoScrollToStart(recyclerView) } @@ -235,11 +231,6 @@ class MainActivity : QkThemedActivity(), MainView { } } - when (state.page is Inbox && state.page.showArchivedSnackbar) { - true -> archiveSnackbar.show() - false -> archiveSnackbar.dismiss() - } - inbox.isSelected = state.page is Inbox archived.isSelected = state.page is Archived @@ -302,6 +293,10 @@ class MainActivity : QkThemedActivity(), MainView { .show() } + override fun showArchivedSnackbar() { + archiveSnackbar.show() + } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.main, menu) return super.onCreateOptionsMenu(menu) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivityModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivityModule.kt index 9e43e652..f87b6c88 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivityModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivityModule.kt @@ -18,6 +18,7 @@ */ package com.moez.QKSMS.feature.main +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModel import com.moez.QKSMS.injection.ViewModelKey import dagger.Module @@ -27,6 +28,9 @@ import dagger.multibindings.IntoMap @Module class MainActivityModule { + @Provides + fun provideLifecycle(activity: MainActivity): Lifecycle = activity.lifecycle + @Provides @IntoMap @ViewModelKey(MainViewModel::class) 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 85ced652..6a89bbd6 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 @@ -42,8 +42,7 @@ data class Inbox( val markPinned: Boolean = true, val markRead: Boolean = false, val data: RealmResults? = null, - val selected: Int = 0, - val showArchivedSnackbar: Boolean = false) : MainPage() + val selected: Int = 0) : MainPage() data class Searching( val loading: Boolean = false, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt index cc7599d4..3ae1484c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt @@ -35,8 +35,8 @@ interface MainView : QkView { val rateIntent: Observable<*> val conversationsSelectedIntent: Observable> val confirmDeleteIntent: Observable - val swipeConversationIntent: Observable - val undoSwipeConversationIntent: Observable + val swipeConversationIntent: Observable> + val undoArchiveIntent: Observable val snackbarButtonIntent: Observable val backPressedIntent: Observable @@ -44,6 +44,7 @@ interface MainView : QkView { fun clearSearch() fun clearSelection() fun showDeleteDialog(count: Int) + fun showArchivedSnackbar() } 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 91cc6cca..d04609a3 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 @@ -18,6 +18,7 @@ */ package com.moez.QKSMS.feature.main +import androidx.recyclerview.widget.ItemTouchHelper import com.moez.QKSMS.R import com.moez.QKSMS.common.Navigator import com.moez.QKSMS.common.androidxcompat.scope @@ -40,6 +41,7 @@ import com.moez.QKSMS.manager.RatingManager import com.moez.QKSMS.model.SyncLog import com.moez.QKSMS.repository.ConversationRepository import com.moez.QKSMS.repository.SyncRepository +import com.moez.QKSMS.util.Preferences import com.uber.autodispose.kotlin.autoDisposable import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers @@ -67,6 +69,7 @@ class MainViewModel @Inject constructor( private val markUnread: MarkUnread, private val navigator: Navigator, private val permissionManager: PermissionManager, + private val prefs: Preferences, private val ratingManager: RatingManager, private val syncMessages: SyncMessages ) : QkViewModel(MainState(page = Inbox(data = conversationRepo.getConversations()))) { @@ -296,7 +299,10 @@ class MainViewModel @Inject constructor( // Delete the conversation view.confirmDeleteIntent - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> conversations } + .withLatestFrom(view.conversationsSelectedIntent, view.swipeConversationIntent) { _, conversations, swipe -> + // If conversations are selected, use those. Otherwise, this was triggered by a swipe-to-delete + conversations.takeIf { it.isNotEmpty() } ?: listOf(swipe.first) + } .autoDisposable(view.scope()) .subscribe { conversations -> deleteConversations.execute(conversations) @@ -304,28 +310,19 @@ class MainViewModel @Inject constructor( } view.swipeConversationIntent - .withLatestFrom(state) { threadId, state -> - markArchived.execute(listOf(threadId)) { - if (state.page is Inbox) { - val page = state.page.copy(showArchivedSnackbar = true) - newState { copy(page = page) } - } - } - } - .switchMap { Observable.timer(2750, TimeUnit.MILLISECONDS) } - .withLatestFrom(state) { threadId, state -> - markArchived.execute(listOf(threadId)) { - if (state.page is Inbox) { - val page = state.page.copy(showArchivedSnackbar = false) - newState { copy(page = page) } - } - } - } .autoDisposable(view.scope()) - .subscribe() + .subscribe { (threadId, direction) -> + val action = if (direction == ItemTouchHelper.RIGHT) prefs.swipeRight.get() else prefs.swipeLeft.get() + when (action) { + Preferences.SWIPE_ACTION_ARCHIVE -> markArchived.execute(listOf(threadId)) { view.showArchivedSnackbar() } + Preferences.SWIPE_ACTION_DELETE -> view.showDeleteDialog(1) + Preferences.SWIPE_ACTION_CALL -> conversationRepo.getConversation(threadId)?.recipients?.firstOrNull()?.address?.let(navigator::makePhoneCall) + Preferences.SWIPE_ACTION_READ -> markRead.execute(listOf(threadId)) + } + } - view.undoSwipeConversationIntent - .withLatestFrom(view.swipeConversationIntent) { _, threadId -> threadId } + view.undoArchiveIntent + .withLatestFrom(view.swipeConversationIntent) { _, pair -> pair.first } .autoDisposable(view.scope()) .subscribe { threadId -> markUnarchived.execute(listOf(threadId)) } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt index 525e3db2..5984699e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt @@ -4,9 +4,7 @@ import android.app.ProgressDialog import android.app.TimePickerDialog import android.content.Context import android.text.format.DateFormat -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.google.android.material.snackbar.Snackbar @@ -21,6 +19,7 @@ import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.common.widget.PreferenceView import com.moez.QKSMS.feature.settings.about.AboutController +import com.moez.QKSMS.feature.settings.swipe.SwipeActionsController import com.moez.QKSMS.feature.themepicker.ThemePickerController import com.moez.QKSMS.injection.appComponent import com.moez.QKSMS.util.Preferences @@ -28,8 +27,8 @@ import com.uber.autodispose.kotlin.autoDisposable import io.reactivex.Observable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject -import kotlinx.android.synthetic.main.settings_activity.* -import kotlinx.android.synthetic.main.settings_activity.view.* +import kotlinx.android.synthetic.main.settings_controller.* +import kotlinx.android.synthetic.main.settings_controller.view.* import kotlinx.android.synthetic.main.settings_switch_widget.view.* import kotlinx.android.synthetic.main.settings_theme_widget.* import javax.inject.Inject @@ -61,7 +60,7 @@ class SettingsController : QkController navigator.showNotificationSettings() + R.id.swipeActions -> view.showSwipeActions() + R.id.delayed -> view.showDelayDurationDialog() R.id.delivery -> prefs.delivery.set(!prefs.delivery.get()) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsView.kt index 0de2a6d6..4e403fff 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsView.kt @@ -39,6 +39,7 @@ interface SettingsView : QkViewContract { fun showTextSizePicker() fun showDelayDurationDialog() fun showMmsSizePicker() + fun showSwipeActions() fun showThemePicker() fun showAbout() } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsController.kt new file mode 100644 index 00000000..ccfa194a --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsController.kt @@ -0,0 +1,99 @@ +/* + * 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.feature.settings.swipe + +import android.view.View +import androidx.core.view.isVisible +import com.jakewharton.rxbinding2.view.clicks +import com.moez.QKSMS.R +import com.moez.QKSMS.common.QkDialog +import com.moez.QKSMS.common.base.QkController +import com.moez.QKSMS.common.util.Colors +import com.moez.QKSMS.common.util.extensions.animateLayoutChanges +import com.moez.QKSMS.common.util.extensions.setBackgroundTint +import com.moez.QKSMS.common.util.extensions.setTint +import com.moez.QKSMS.injection.appComponent +import com.uber.autodispose.kotlin.autoDisposable +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject +import kotlinx.android.synthetic.main.swipe_actions_controller.* +import javax.inject.Inject + +class SwipeActionsController : QkController(), SwipeActionsView { + + @Inject override lateinit var presenter: SwipeActionsPresenter + @Inject lateinit var actionsDialog: QkDialog + @Inject lateinit var colors: Colors + + /** + * Allows us to subscribe to [actionClicks] more than once + */ + private val actionClicks: Subject = PublishSubject.create() + + init { + appComponent.inject(this) + layoutRes = R.layout.swipe_actions_controller + + actionsDialog.adapter.setData(R.array.settings_swipe_actions) + } + + override fun onAttach(view: View) { + super.onAttach(view) + presenter.bindIntents(this) + setTitle(R.string.settings_swipe_actions) + showBackButton(true) + + colors.theme().let { theme -> + rightIcon.setBackgroundTint(theme.theme) + rightIcon.setTint(theme.textPrimary) + leftIcon.setBackgroundTint(theme.theme) + leftIcon.setTint(theme.textPrimary) + } + + right.postDelayed({ right.animateLayoutChanges = true }, 100) + left.postDelayed({ left.animateLayoutChanges = true }, 100) + + Observable.merge( + right.clicks().map { SwipeActionsView.Action.RIGHT }, + left.clicks().map { SwipeActionsView.Action.LEFT }) + .autoDisposable(scope()) + .subscribe(actionClicks) + } + + override fun actionClicks(): Observable = actionClicks + + override fun actionSelected(): Observable = actionsDialog.adapter.menuItemClicks + + override fun showSwipeActions(selected: Int) { + actionsDialog.adapter.selectedItem = selected + activity?.let(actionsDialog::show) + } + + override fun render(state: SwipeActionsState) { + rightIcon.isVisible = state.rightIcon != 0 + rightIcon.setImageResource(state.rightIcon) + rightLabel.text = state.rightLabel + + leftIcon.isVisible = state.leftIcon != 0 + leftIcon.setImageResource(state.leftIcon) + leftLabel.text = state.leftLabel + } + +} \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsPresenter.kt new file mode 100644 index 00000000..f7fbf787 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsPresenter.kt @@ -0,0 +1,79 @@ +/* + * 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.feature.settings.swipe + +import android.content.Context +import androidx.annotation.DrawableRes +import com.moez.QKSMS.R +import com.moez.QKSMS.common.base.QkPresenter +import com.moez.QKSMS.util.Preferences +import com.uber.autodispose.kotlin.autoDisposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.withLatestFrom +import javax.inject.Inject + +class SwipeActionsPresenter @Inject constructor( + context: Context, + private val prefs: Preferences +) : QkPresenter(SwipeActionsState()) { + + init { + val actionLabels = context.resources.getStringArray(R.array.settings_swipe_actions) + + disposables += prefs.swipeRight.asObservable() + .subscribe { action -> newState { copy(rightLabel = actionLabels[action], rightIcon = iconForAction(action)) } } + + disposables += prefs.swipeLeft.asObservable() + .subscribe { action -> newState { copy(leftLabel = actionLabels[action], leftIcon = iconForAction(action)) } } + } + + override fun bindIntents(view: SwipeActionsView) { + super.bindIntents(view) + + view.actionClicks() + .map { action -> + when (action) { + SwipeActionsView.Action.RIGHT -> prefs.swipeRight.get() + SwipeActionsView.Action.LEFT -> prefs.swipeLeft.get() + } + } + .autoDisposable(view.scope()) + .subscribe(view::showSwipeActions) + + view.actionSelected() + .withLatestFrom(view.actionClicks()) { actionId, action -> + when (action) { + SwipeActionsView.Action.RIGHT -> prefs.swipeRight.set(actionId) + SwipeActionsView.Action.LEFT -> prefs.swipeLeft.set(actionId) + } + } + .autoDisposable(view.scope()) + .subscribe() + } + + @DrawableRes + private fun iconForAction(action: Int) = when (action) { + Preferences.SWIPE_ACTION_ARCHIVE -> R.drawable.ic_archive_black_24dp + Preferences.SWIPE_ACTION_DELETE -> R.drawable.ic_delete_white_24dp + Preferences.SWIPE_ACTION_CALL -> R.drawable.ic_call_white_24dp + Preferences.SWIPE_ACTION_READ -> R.drawable.ic_check_white_24dp + else -> 0 + } + +} \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsState.kt new file mode 100644 index 00000000..360260cb --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsState.kt @@ -0,0 +1,30 @@ +/* + * 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.feature.settings.swipe + +import androidx.annotation.DrawableRes +import com.moez.QKSMS.R + +data class SwipeActionsState( + @DrawableRes val rightIcon: Int = R.drawable.ic_archive_black_24dp, + val rightLabel: String = "", + + @DrawableRes val leftIcon: Int = R.drawable.ic_archive_black_24dp, + val leftLabel: String = "" +) \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsView.kt new file mode 100644 index 00000000..e8579195 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsView.kt @@ -0,0 +1,33 @@ +/* + * 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.feature.settings.swipe + +import com.moez.QKSMS.common.base.QkViewContract +import io.reactivex.Observable + +interface SwipeActionsView : QkViewContract { + + enum class Action { LEFT, RIGHT } + + fun actionClicks(): Observable + fun actionSelected(): Observable + + fun showSwipeActions(selected: Int) + +} \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt b/presentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt index 26c4a9e8..0c2d5753 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt @@ -29,6 +29,7 @@ import com.moez.QKSMS.common.widget.QkTextView import com.moez.QKSMS.feature.compose.DetailedChipView import com.moez.QKSMS.feature.settings.SettingsController import com.moez.QKSMS.feature.settings.about.AboutController +import com.moez.QKSMS.feature.settings.swipe.SwipeActionsController import com.moez.QKSMS.feature.themepicker.injection.ThemePickerComponent import com.moez.QKSMS.feature.widget.WidgetAdapter import com.moez.QKSMS.injection.android.ActivityBuilderModule @@ -54,6 +55,7 @@ interface AppComponent { fun inject(controller: AboutController) fun inject(controller: SettingsController) + fun inject(controller: SwipeActionsController) fun inject(dialog: QkDialog) diff --git a/presentation/src/main/res/drawable/ic_clear_all_black_24dp.xml b/presentation/src/main/res/drawable/ic_clear_all_black_24dp.xml new file mode 100644 index 00000000..341c6cb8 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_clear_all_black_24dp.xml @@ -0,0 +1,27 @@ + + + + diff --git a/presentation/src/main/res/drawable/rounded_rectangle_left_4dp.xml b/presentation/src/main/res/drawable/rounded_rectangle_left_4dp.xml new file mode 100755 index 00000000..301caa17 --- /dev/null +++ b/presentation/src/main/res/drawable/rounded_rectangle_left_4dp.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/rounded_rectangle_outline_4dp.xml b/presentation/src/main/res/drawable/rounded_rectangle_outline_4dp.xml new file mode 100755 index 00000000..42afb2c5 --- /dev/null +++ b/presentation/src/main/res/drawable/rounded_rectangle_outline_4dp.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/drawable/rounded_rectangle_right_4dp.xml b/presentation/src/main/res/drawable/rounded_rectangle_right_4dp.xml new file mode 100755 index 00000000..c6f30c93 --- /dev/null +++ b/presentation/src/main/res/drawable/rounded_rectangle_right_4dp.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/settings_activity.xml b/presentation/src/main/res/layout/settings_controller.xml similarity index 95% rename from presentation/src/main/res/layout/settings_activity.xml rename to presentation/src/main/res/layout/settings_controller.xml index 7a97426b..0e75d793 100644 --- a/presentation/src/main/res/layout/settings_activity.xml +++ b/presentation/src/main/res/layout/settings_controller.xml @@ -149,6 +149,15 @@ app:title="@string/settings_delayed_title" tools:summary="No delay" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 13a340aa..02b6732f 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -187,6 +187,18 @@ Should I Answer? Automatically filter messages from unsolicited numbers by using the \"Should I Answer\" app Delayed sending + Swipe actions + Configure swipe actions for conversations + Right swipe + Left swipe + CHANGE + + None + Archive + Delete + Call + Mark read + Delivery confirmations Confirm that messages were sent successfully Strip accents