diff --git a/data/src/main/java/com/moez/QKSMS/manager/WidgetManagerImpl.kt b/data/src/main/java/com/moez/QKSMS/manager/WidgetManagerImpl.kt index 7c19f55a..c1352447 100644 --- a/data/src/main/java/com/moez/QKSMS/manager/WidgetManagerImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/manager/WidgetManagerImpl.kt @@ -23,17 +23,45 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import com.klinker.android.send_message.BroadcastUtils +import dev.octoshrimpy.quik.util.Preferences +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject -class WidgetManagerImpl @Inject constructor(private val context: Context) : WidgetManager { +class WidgetManagerImpl @Inject constructor( + private val context: Context, + private val prefs: Preferences + ) : WidgetManager { - override fun updateUnreadCount() { - BroadcastUtils.sendExplicitBroadcast(context, Intent(), WidgetManager.ACTION_NOTIFY_DATASET_CHANGED) + companion object { + private var staticUnreadAtTopPrefsDisposable = AtomicReference(null) + + fun sendDatasetChanged(context: Context) { + BroadcastUtils.sendExplicitBroadcast(context, Intent(), WidgetManager.ACTION_NOTIFY_DATASET_CHANGED) + } + } + + init { + // this class is always instantiated by it's factory with the application context so we can capture that context value below + // when unreadAtTop preference changes, send broadcast to widgets to update themselves + if (staticUnreadAtTopPrefsDisposable.get() === null) + staticUnreadAtTopPrefsDisposable.set(prefs.unreadAtTop.asObservable() + .skip(1) + .debounce(400, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { sendDatasetChanged(context) } + .subscribe()) + } + + override fun sendDatasetChanged() { + sendDatasetChanged(context) } override fun updateTheme() { val ids = AppWidgetManager.getInstance(context) - .getAppWidgetIds(ComponentName("dev.octoshrimpy.quik", "dev.octoshrimpy.quik.feature.widget.WidgetProvider")) + .getAppWidgetIds(ComponentName(context.packageName, "dev.octoshrimpy.quik.feature.widget.WidgetProvider")) val intent = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) 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 9ef8d244..324d975c 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt @@ -56,7 +56,15 @@ class ConversationRepositoryImpl @Inject constructor( private val phoneNumberUtils: PhoneNumberUtils ) : ConversationRepository { - override fun getConversations(archived: Boolean): RealmResults { + override fun getConversations(unreadAtTop: Boolean, archived: Boolean): RealmResults { + val sortOrder: MutableList = arrayListOf("pinned", "draft", "lastMessage.date") + val sortDirections: MutableList = arrayListOf(Sort.DESCENDING, Sort.DESCENDING, Sort.DESCENDING) + + if (unreadAtTop) { + sortOrder.add(0, "lastMessage.read") + sortDirections.add(0, Sort.ASCENDING) + } + return Realm.getDefaultInstance() .where(Conversation::class.java) .notEqualTo("id", 0L) @@ -69,13 +77,21 @@ class ConversationRepositoryImpl @Inject constructor( .isNotEmpty("draft") .endGroup() .sort( - arrayOf("pinned", "draft", "lastMessage.date"), - arrayOf(Sort.DESCENDING, Sort.DESCENDING, Sort.DESCENDING) + sortOrder.toTypedArray(), + sortDirections.toTypedArray() ) .findAllAsync() } - override fun getConversationsSnapshot(): List { + override fun getConversationsSnapshot(unreadAtTop: Boolean): List { + val sortOrder: MutableList = arrayListOf("pinned", "draft", "lastMessage.date") + val sortDirections: MutableList = arrayListOf(Sort.DESCENDING, Sort.DESCENDING, Sort.DESCENDING) + + if (unreadAtTop) { + sortOrder.add(0, "lastMessage.read") + sortDirections.add(0, Sort.ASCENDING) + } + return Realm.getDefaultInstance().use { realm -> realm.refresh() realm.copyFromRealm(realm.where(Conversation::class.java) @@ -89,8 +105,8 @@ class ConversationRepositoryImpl @Inject constructor( .isNotEmpty("draft") .endGroup() .sort( - arrayOf("pinned", "draft", "lastMessage.date"), - arrayOf(Sort.DESCENDING, Sort.DESCENDING, Sort.DESCENDING) + sortOrder.toTypedArray(), + sortDirections.toTypedArray() ) .findAll()) } diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/UpdateBadge.kt b/domain/src/main/java/com/moez/QKSMS/interactor/UpdateBadge.kt index c7dfec7f..d1c051b0 100644 --- a/domain/src/main/java/com/moez/QKSMS/interactor/UpdateBadge.kt +++ b/domain/src/main/java/com/moez/QKSMS/interactor/UpdateBadge.kt @@ -31,7 +31,7 @@ class UpdateBadge @Inject constructor( override fun buildObservable(params: Unit): Flowable<*> { return Flowable.just(params) .doOnNext { shortcutManager.updateBadge() } - .doOnNext { widgetManager.updateUnreadCount() } + .doOnNext { widgetManager.sendDatasetChanged() } } } \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/manager/WidgetManager.kt b/domain/src/main/java/com/moez/QKSMS/manager/WidgetManager.kt index 1963f2e7..f6662546 100644 --- a/domain/src/main/java/com/moez/QKSMS/manager/WidgetManager.kt +++ b/domain/src/main/java/com/moez/QKSMS/manager/WidgetManager.kt @@ -24,7 +24,7 @@ interface WidgetManager { const val ACTION_NOTIFY_DATASET_CHANGED = "dev.octoshrimpy.quik.intent.action.ACTION_NOTIFY_DATASET_CHANGED" } - fun updateUnreadCount() + fun sendDatasetChanged() fun updateTheme() 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 77cf4130..2a34b72f 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt @@ -27,9 +27,9 @@ import io.realm.RealmResults interface ConversationRepository { - fun getConversations(archived: Boolean = false): RealmResults + fun getConversations(unreadAtTop: Boolean, archived: Boolean = false): RealmResults - fun getConversationsSnapshot(): List + fun getConversationsSnapshot(unreadAtTop: Boolean): List /** * Returns the top conversations that were active in the last week diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index 23607bbb..86a99abc 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -47,6 +47,7 @@ class Preferences @Inject constructor( const val TEXT_SIZE_NORMAL = 1 const val TEXT_SIZE_LARGE = 2 const val TEXT_SIZE_LARGER = 3 + const val TEXT_SIZE_SUPER = 4 const val NOTIFICATION_PREVIEWS_ALL = 0 const val NOTIFICATION_PREVIEWS_NAME = 1 @@ -123,6 +124,7 @@ class Preferences @Inject constructor( val longAsMms = rxPrefs.getBoolean("longAsMms", false) val mmsSize = rxPrefs.getInteger("mmsSize", 300) val logging = rxPrefs.getBoolean("logging", false) + val unreadAtTop = rxPrefs.getBoolean("unreadAtTop", false) init { // Migrate from old night mode preference to new one, now that we support android Q night mode diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/TextViewStyler.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/TextViewStyler.kt index 4b7f90d3..9e4d110d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/TextViewStyler.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/TextViewStyler.kt @@ -153,6 +153,7 @@ class TextViewStyler @Inject constructor( Preferences.TEXT_SIZE_NORMAL -> 16f Preferences.TEXT_SIZE_LARGE -> 18f Preferences.TEXT_SIZE_LARGER -> 20f + Preferences.TEXT_SIZE_SUPER -> 40f else -> 16f } @@ -161,6 +162,7 @@ class TextViewStyler @Inject constructor( Preferences.TEXT_SIZE_NORMAL -> 14f Preferences.TEXT_SIZE_LARGE -> 16f Preferences.TEXT_SIZE_LARGER -> 18f + Preferences.TEXT_SIZE_SUPER -> 36f else -> 14f } @@ -169,6 +171,7 @@ class TextViewStyler @Inject constructor( Preferences.TEXT_SIZE_NORMAL -> 12f Preferences.TEXT_SIZE_LARGE -> 14f Preferences.TEXT_SIZE_LARGER -> 16f + Preferences.TEXT_SIZE_SUPER -> 32f else -> 12f } @@ -177,6 +180,7 @@ class TextViewStyler @Inject constructor( Preferences.TEXT_SIZE_NORMAL -> 20f Preferences.TEXT_SIZE_LARGE -> 22f Preferences.TEXT_SIZE_LARGER -> 26f + Preferences.TEXT_SIZE_SUPER -> 52f else -> 20f } @@ -185,6 +189,7 @@ class TextViewStyler @Inject constructor( Preferences.TEXT_SIZE_NORMAL -> 18f Preferences.TEXT_SIZE_LARGE -> 20f Preferences.TEXT_SIZE_LARGER -> 24f + Preferences.TEXT_SIZE_SUPER -> 48f else -> 18f } @@ -193,6 +198,7 @@ class TextViewStyler @Inject constructor( Preferences.TEXT_SIZE_NORMAL -> 32f Preferences.TEXT_SIZE_LARGE -> 36f Preferences.TEXT_SIZE_LARGER -> 40f + Preferences.TEXT_SIZE_SUPER -> 80f else -> 32f } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt index d16745f2..8119e98c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt @@ -21,15 +21,17 @@ package dev.octoshrimpy.quik.common.util.extensions import android.animation.LayoutTransition import android.content.Context import android.content.res.ColorStateList -import android.graphics.PorterDuff -import android.os.Build +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent import android.view.View +import android.view.View.OnTouchListener import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.ImageView import android.widget.ProgressBar +import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.ViewPager @@ -83,29 +85,53 @@ fun View.setVisible(visible: Boolean, invisible: Int = View.GONE) { */ fun View.forwardTouches(parent: View) { var isLongClick = false + var textInitiallySelectable = false + if (this@forwardTouches is TextView) + textInitiallySelectable = this@forwardTouches.isTextSelectable - setOnLongClickListener { - isLongClick = true - true - } + setOnTouchListener(object : OnTouchListener { + private val gestureDetector = + GestureDetector(parent.context, object : SimpleOnGestureListener() { + private var lastUpEvent: MotionEvent? = null - setOnTouchListener { v, event -> - parent.onTouchEvent(event) + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + parent.onTouchEvent(e) + if (lastUpEvent !== null) { + parent.onTouchEvent(lastUpEvent) + lastUpEvent = null + } - when { - event.action == MotionEvent.ACTION_UP && isLongClick -> { - isLongClick = true - true - } + return true + } - event.action == MotionEvent.ACTION_DOWN -> { - isLongClick = false - v.onTouchEvent(event) - } + override fun onSingleTapUp(e: MotionEvent): Boolean { + onTouchEvent(e) + lastUpEvent = e + return true + } - else -> v.onTouchEvent(event) + override fun onDown(e: MotionEvent): Boolean { + isLongClick = false + onTouchEvent(e) + return true + } + + override fun onLongPress(e: MotionEvent) { + parent.onTouchEvent(e) + isLongClick = true + // this is kinda odd but we have to 'bounce' the text selectable value so it doesn't + // start selecting text on a long press, but will start selecting it on the next double-tap + if (this@forwardTouches is TextView) { + (this@forwardTouches as TextView).setTextIsSelectable(false) + (this@forwardTouches as TextView).setTextIsSelectable(textInitiallySelectable) + } + } + }) + + override fun onTouch(v: View, e: MotionEvent): Boolean { + return gestureDetector.onTouchEvent(e) } - } + }) } fun ViewPager.addOnPageChangeListener(listener: (Int) -> Unit) { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt index dd6fcf06..7a786893 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt @@ -136,7 +136,7 @@ class ComposeViewModel @Inject constructor( } // Otherwise, we'll monitor the conversations until our expected conversation is created - conversationRepo.getConversations().asObservable() + conversationRepo.getConversations(prefs.unreadAtTop.get()).asObservable() .filter { it.isLoaded } .observeOn(Schedulers.io()) .map { conversationRepo.getOrCreateConversation(addresses)?.id ?: 0 } 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 03439ef7..2e8f4ba4 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 @@ -83,7 +83,7 @@ class MainViewModel @Inject constructor( private val ratingManager: RatingManager, private val syncContacts: SyncContacts, private val syncMessages: SyncMessages -) : QkViewModel(MainState(page = Inbox(data = conversationRepo.getConversations()))) { +) : QkViewModel(MainState(page = Inbox(data = conversationRepo.getConversations(prefs.unreadAtTop.get())))) { init { disposables += deleteConversations @@ -140,6 +140,25 @@ class MainViewModel @Inject constructor( !permissionManager.hasReadSms() || !permissionManager.hasContacts() -> view.requestPermissions() } + + // when unreadAtTop preference changes, reload the model view data to refresh view + prefs.unreadAtTop.asObservable() + .skip(1) + .debounce(400, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .withLatestFrom(state) { _, state -> + if (state.page is Inbox) + newState { + copy(page = Inbox(data = conversationRepo.getConversations(prefs.unreadAtTop.get()))) + } + else if (state.page is Archived) + newState { + copy(page = Inbox(data = conversationRepo.getConversations(prefs.unreadAtTop.get(), true))) + } + } + .autoDisposable(view.scope()) + .subscribe() + // If the default SMS state changes, reflect it in the State view.activityResumedIntent .filter { resumed -> resumed } @@ -226,7 +245,7 @@ class MainViewModel @Inject constructor( .map { query -> query.trim() } .withLatestFrom(state) { query, state -> if (query.isEmpty() && state.page is Searching) { - newState { copy(page = Inbox(data = conversationRepo.getConversations())) } + newState { copy(page = Inbox(data = conversationRepo.getConversations(prefs.unreadAtTop.get()))) } } query } @@ -288,7 +307,7 @@ class MainViewModel @Inject constructor( state.page is Inbox && state.page.selected > 0 -> view.clearSelection() state.page is Archived && state.page.selected > 0 -> view.clearSelection() state.page !is Inbox -> { - newState { copy(page = Inbox(data = conversationRepo.getConversations())) } + newState { copy(page = Inbox(data = conversationRepo.getConversations(prefs.unreadAtTop.get()))) } } else -> newState { copy(hasError = true) } } @@ -306,8 +325,8 @@ class MainViewModel @Inject constructor( .distinctUntilChanged() .doOnNext { drawerItem -> when (drawerItem) { - NavItem.INBOX -> newState { copy(page = Inbox(data = conversationRepo.getConversations())) } - NavItem.ARCHIVED -> newState { copy(page = Archived(data = conversationRepo.getConversations(true))) } + NavItem.INBOX -> newState { copy(page = Inbox(data = conversationRepo.getConversations(prefs.unreadAtTop.get()))) } + NavItem.ARCHIVED -> newState { copy(page = Archived(data = conversationRepo.getConversations(prefs.unreadAtTop.get(), true))) } else -> Unit } } 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 d8b029a9..ff03200f 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 @@ -169,6 +169,8 @@ class SettingsController : QkController newState { copy(sendDelaySummary = delayedSendingLabels[id], sendDelayId = id) } } disposables += prefs.delivery.asObservable() - .subscribe { enabled -> newState { copy(deliveryEnabled = enabled) } } + .subscribe { enabled -> newState { copy(deliveryEnabled = enabled) } } + + disposables += prefs.unreadAtTop.asObservable() + .subscribe { enabled -> newState { copy(unreadAtTopEnabled = enabled) } } disposables += prefs.signature.asObservable() .subscribe { signature -> newState { copy(signature = signature) } } @@ -178,6 +181,8 @@ class SettingsPresenter @Inject constructor( R.id.delivery -> prefs.delivery.set(!prefs.delivery.get()) + R.id.unreadAtTop -> prefs.unreadAtTop.set(!prefs.unreadAtTop.get()) + R.id.signature -> view.showSignatureDialog(prefs.signature.get()) R.id.textSize -> view.showTextSizePicker() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt index 144a0696..ea90b728 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt @@ -34,6 +34,7 @@ data class SettingsState( val sendDelaySummary: String = "", val sendDelayId: Int = 0, val deliveryEnabled: Boolean = false, + val unreadAtTopEnabled: Boolean = false, val signature: String = "", val textSizeSummary: String = "", val textSizeId: Int = Preferences.TEXT_SIZE_NORMAL, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt index 42f31650..9d355bfc 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt @@ -83,7 +83,7 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { } override fun onDataSetChanged() { - conversations = conversationRepo.getConversationsSnapshot() + conversations = conversationRepo.getConversationsSnapshot(prefs.unreadAtTop.get()) val remoteViews = RemoteViews(context.packageName, R.layout.widget) appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews) diff --git a/presentation/src/main/res/drawable/ic_unreadattop_all_black_24dp.xml b/presentation/src/main/res/drawable/ic_unreadattop_all_black_24dp.xml new file mode 100644 index 00000000..d3875952 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_unreadattop_all_black_24dp.xml @@ -0,0 +1,27 @@ + + + + diff --git a/presentation/src/main/res/layout/message_list_item_in.xml b/presentation/src/main/res/layout/message_list_item_in.xml index b47aea82..252af7a3 100644 --- a/presentation/src/main/res/layout/message_list_item_in.xml +++ b/presentation/src/main/res/layout/message_list_item_in.xml @@ -98,6 +98,7 @@ android:background="@drawable/message_only" android:gravity="start|center_vertical" android:minHeight="36dp" + android:textIsSelectable="true" app:layout_constrainedWidth="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0" diff --git a/presentation/src/main/res/layout/message_list_item_out.xml b/presentation/src/main/res/layout/message_list_item_out.xml index c20f667c..649e57d8 100644 --- a/presentation/src/main/res/layout/message_list_item_out.xml +++ b/presentation/src/main/res/layout/message_list_item_out.xml @@ -92,6 +92,7 @@ android:backgroundTint="?attr/bubbleColor" android:gravity="start|center_vertical" android:minHeight="36dp" + android:textIsSelectable="true" app:layout_constrainedWidth="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" diff --git a/presentation/src/main/res/layout/settings_controller.xml b/presentation/src/main/res/layout/settings_controller.xml index 1a62cb28..19c392ed 100644 --- a/presentation/src/main/res/layout/settings_controller.xml +++ b/presentation/src/main/res/layout/settings_controller.xml @@ -152,6 +152,15 @@ app:title="@string/settings_delivery_title" app:widget="@layout/settings_switch_widget" /> + + Delivery confirmations Confirm that messages were sent successfully + Unread at the top + Show unread conversations at the top Signature Add a signature to the end of your messages Strip accents @@ -452,6 +454,7 @@ Normal Large Larger + Super