Merge pull request #213 from gavine99/unread_at_top

make unread conversations float at the top of conversation lists
This commit is contained in:
octoshrimpy
2025-01-14 08:16:25 -07:00
committed by GitHub
15 changed files with 132 additions and 25 deletions
@@ -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<Disposable>(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)
@@ -56,7 +56,15 @@ class ConversationRepositoryImpl @Inject constructor(
private val phoneNumberUtils: PhoneNumberUtils
) : ConversationRepository {
override fun getConversations(archived: Boolean): RealmResults<Conversation> {
override fun getConversations(unreadAtTop: Boolean, archived: Boolean): RealmResults<Conversation> {
val sortOrder: MutableList<String> = arrayListOf("pinned", "draft", "lastMessage.date")
val sortDirections: MutableList<Sort> = 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<Conversation> {
override fun getConversationsSnapshot(unreadAtTop: Boolean): List<Conversation> {
val sortOrder: MutableList<String> = arrayListOf("pinned", "draft", "lastMessage.date")
val sortDirections: MutableList<Sort> = 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())
}
@@ -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() }
}
}
@@ -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()
@@ -27,9 +27,9 @@ import io.realm.RealmResults
interface ConversationRepository {
fun getConversations(archived: Boolean = false): RealmResults<Conversation>
fun getConversations(unreadAtTop: Boolean, archived: Boolean = false): RealmResults<Conversation>
fun getConversationsSnapshot(): List<Conversation>
fun getConversationsSnapshot(unreadAtTop: Boolean): List<Conversation>
/**
* Returns the top conversations that were active in the last week
@@ -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
@@ -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 }
@@ -83,7 +83,7 @@ class MainViewModel @Inject constructor(
private val ratingManager: RatingManager,
private val syncContacts: SyncContacts,
private val syncMessages: SyncMessages
) : QkViewModel<MainView, MainState>(MainState(page = Inbox(data = conversationRepo.getConversations()))) {
) : QkViewModel<MainView, MainState>(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
}
}
@@ -169,6 +169,8 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
delivery.checkbox.isChecked = state.deliveryEnabled
unreadAtTop.checkbox.isChecked = state.unreadAtTopEnabled
signature.summary = state.signature.takeIf { it.isNotBlank() }
?: context.getString(R.string.settings_signature_summary)
@@ -98,7 +98,10 @@ class SettingsPresenter @Inject constructor(
.subscribe { id -> 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()
@@ -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,
@@ -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)
@@ -0,0 +1,27 @@
<!--
~ Copyright (C) 2019 Moez Bhatti <moez.bhatti@gmail.com>
~
~ 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 <http://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M5,8h14v-3L5,5v2z,M5,13h14v-3L5,10v2z,M5,16h14v-1L5,15v2z,M5,19h14v-1L5,18v2z" />
</vector>
@@ -152,6 +152,15 @@
app:title="@string/settings_delivery_title"
app:widget="@layout/settings_switch_widget" />
<dev.octoshrimpy.quik.common.widget.PreferenceView
android:id="@+id/unreadAtTop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:icon="@drawable/ic_unreadattop_all_black_24dp"
app:summary="@string/settings_unread_at_top_summary"
app:title="@string/settings_unread_at_top_title"
app:widget="@layout/settings_switch_widget" />
<dev.octoshrimpy.quik.common.widget.PreferenceView
android:id="@+id/signature"
android:layout_width="match_parent"
@@ -259,6 +259,8 @@
</string-array>
<string name="settings_delivery_title">Delivery confirmations</string>
<string name="settings_delivery_summary">Confirm that messages were sent successfully</string>
<string name="settings_unread_at_top_title">Unread at the top</string>
<string name="settings_unread_at_top_summary">Show unread conversations at the top</string>
<string name="settings_signature_title">Signature</string>
<string name="settings_signature_summary">Add a signature to the end of your messages</string>
<string name="settings_unicode_title">Strip accents</string>