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..6599a2f2 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -123,6 +123,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/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 4667caea..e2dae285 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 @@ -19,7 +19,6 @@ package dev.octoshrimpy.quik.feature.widget import android.appwidget.AppWidgetManager -import android.content.ComponentName import android.content.Context import android.content.Intent import android.text.SpannableStringBuilder @@ -34,8 +33,6 @@ import dev.octoshrimpy.quik.common.util.Colors import dev.octoshrimpy.quik.common.util.DateFormatter import dev.octoshrimpy.quik.common.util.extensions.dpToPx import dev.octoshrimpy.quik.common.util.extensions.getColorCompat -import dev.octoshrimpy.quik.feature.compose.ComposeActivity -import dev.octoshrimpy.quik.feature.main.MainActivity import dev.octoshrimpy.quik.injection.appComponent import dev.octoshrimpy.quik.model.Contact import dev.octoshrimpy.quik.model.Conversation @@ -85,7 +82,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/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