Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental: Replace 'Clear' button with a 'Swap' button (between two presets) #3848

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,008 changes: 1,008 additions & 0 deletions app/schemas/com.keylesspalace.tusky.db.AppDatabase/52.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ class NotificationsFragment :
// Clear behaviour to hide app bar
params.behavior = null
}
// Set name of "Swap" button
// FIXME: This politely skips if filters has not yet been populated, BUT if filters or filterIndex ever get set to "strange" values it could impolitely crash.
if (viewModel.uiState.value.filters.size > 0) {
val offset =
viewModel.uiState.value.filters[viewModel.uiState.value.filterIndex].size - viewModel.uiState.value.filters[1 - viewModel.uiState.value.filterIndex].size
val swapText = if (offset < 0) {
R.string.notifications_swap_less
} else if (offset > 0) {
R.string.notifications_swap_more
} else {
R.string.notifications_swap
}
binding.buttonSwap.setText(swapText)
}
}

private fun confirmClearNotifications() {
Expand Down Expand Up @@ -215,7 +229,8 @@ class NotificationsFragment :
footer = NotificationsLoadStateAdapter { adapter.retry() }
)

binding.buttonClear.setOnClickListener { confirmClearNotifications() }
// binding.buttonClear.setOnClickListener { confirmClearNotifications() }
binding.buttonSwap.setOnClickListener { swapNotifications() }
binding.buttonFilter.setOnClickListener { showFilterDialog() }
(binding.recyclerView.itemAnimator as SimpleItemAnimator?)!!.supportsChangeAnimations =
false
Expand Down Expand Up @@ -572,10 +587,18 @@ class NotificationsFragment :
viewModel.accept(FallibleUiAction.ClearNotifications)
}

private fun swapNotifications() {
binding.swipeRefreshLayout.isRefreshing = false
binding.progressBar.isVisible = false
viewModel.accept(InfallibleUiAction.ActiveFilter(1 - viewModel.uiState.value.filterIndex))
}

private fun showFilterDialog() {
FilterDialogFragment(viewModel.uiState.value.activeFilter) { filter ->
if (viewModel.uiState.value.activeFilter != filter) {
viewModel.accept(InfallibleUiAction.ApplyFilter(filter))
val filters = viewModel.uiState.value.filters.copyOf()
filters[viewModel.uiState.value.filterIndex] = filter
viewModel.accept(InfallibleUiAction.ApplyFilters(filters))
}
}
.show(parentFragmentManager, "dialogFilter")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,13 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime

data class UiState(
/** Filtered notification types */
/** All saved sets for filtered notification types */
val filters: Array<Set<Notification.Type>> = emptyArray(),

/** Currently active index within filters */
val filterIndex: Int = 0,

/** Filtered notification types-- should be a reference into filters */
val activeFilter: Set<Notification.Type> = emptySet(),

/** True if the UI to filter and clear notifications should be shown */
Expand Down Expand Up @@ -115,7 +121,10 @@ sealed class InfallibleUiAction : UiAction() {
// This saves the list to the local database, which triggers a refresh of the data.
// Saving the data can't fail, which is why this is infallible. Refreshing the
// data may fail, but that's handled by the paging system / adapter refresh logic.
data class ApplyFilter(val filter: Set<Notification.Type>) : InfallibleUiAction()
data class ApplyFilters(val filters: Array<Set<Notification.Type>>) : InfallibleUiAction()

/** Select which of the two filters are active */
data class ActiveFilter(val active: Int) : InfallibleUiAction()

/**
* User is leaving the fragment, save the ID of the visible notification.
Expand Down Expand Up @@ -334,20 +343,38 @@ class NotificationsViewModel @Inject constructor(

init {
// Handle changes to notification filters
val notificationFilter = uiAction
.filterIsInstance<InfallibleUiAction.ApplyFilter>()
val notificationFilters = uiAction
.filterIsInstance<InfallibleUiAction.ApplyFilters>()
.distinctUntilChanged()
// Save each change back to the active account
.onEach { action ->
Log.d(TAG, "notificationFilters: $action")
account.notificationsFilters = serialize(action.filters)
accountManager.saveAccount(account)
}
// Load the initial filter from the active account
.onStart {
emit(
InfallibleUiAction.ApplyFilters(
filters = deserialize(account.notificationsFilters)
)
)
}

val notificationFilterActive = uiAction
.filterIsInstance<InfallibleUiAction.ActiveFilter>()
.distinctUntilChanged()
// Save each change back to the active account
.onEach { action ->
Log.d(TAG, "notificationFilter: $action")
account.notificationsFilter = serialize(action.filter)
Log.d(TAG, "notificationsFilterIndex: $action")
account.notificationsFilterIndex = action.active
accountManager.saveAccount(account)
}
// Load the initial filter from the active account
.onStart {
emit(
InfallibleUiAction.ApplyFilter(
filter = deserialize(account.notificationsFilter)
InfallibleUiAction.ActiveFilter(
active = account.notificationsFilterIndex
)
)
}
Expand Down Expand Up @@ -489,14 +516,16 @@ class NotificationsViewModel @Inject constructor(

// Re-fetch notifications if either of `notificationFilter` or `reload` flows have
// new items.
pagingData = combine(notificationFilter, reload) { action, _ -> action }
pagingData = combine(notificationFilters, notificationFilterActive, reload) { filters, filterIndex, _ -> filters.filters[filterIndex.active] }
.flatMapLatest { action ->
getNotifications(filters = action.filter, initialKey = getInitialKey())
getNotifications(filters = action, initialKey = getInitialKey())
}.cachedIn(viewModelScope)

uiState = combine(notificationFilter, getUiPrefs()) { filter, prefs ->
uiState = combine(notificationFilters, notificationFilterActive, getUiPrefs()) { filters, filterIndex, prefs ->
UiState(
activeFilter = filter.filter,
filters = filters.filters,
filterIndex = filterIndex.active,
activeFilter = filters.filters[filterIndex.active],
showFilterOptions = prefs.showFilter,
showFabWhileScrolling = prefs.showFabWhileScrolling
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,19 +267,23 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS
setOnPreferenceChangeListener { _, value ->
for (account in accountManager.accounts) {
val notificationFilter = deserialize(account.notificationsFilter).toMutableSet()

if (value == true) {
notificationFilter.add(Notification.Type.FAVOURITE)
notificationFilter.add(Notification.Type.FOLLOW)
notificationFilter.add(Notification.Type.REBLOG)
} else {
notificationFilter.remove(Notification.Type.FAVOURITE)
notificationFilter.remove(Notification.Type.FOLLOW)
notificationFilter.remove(Notification.Type.REBLOG)
val notificationFilters = deserialize(account.notificationsFilters)
for (idx in 0..notificationFilters.size) {
var notificationFilter = notificationFilters[idx].toMutableSet()

if (value == true) {
notificationFilter.add(Notification.Type.FAVOURITE)
notificationFilter.add(Notification.Type.FOLLOW)
notificationFilter.add(Notification.Type.REBLOG)
} else {
notificationFilter.remove(Notification.Type.FAVOURITE)
notificationFilter.remove(Notification.Type.FOLLOW)
notificationFilter.remove(Notification.Type.REBLOG)
}
notificationFilters[idx] = notificationFilter
}

account.notificationsFilter = serialize(notificationFilter)
account.notificationsFilters = serialize(notificationFilters)
accountManager.saveAccount(account)
}
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ data class AccountEntity(
var notificationMarkerId: String = "0",
var emojis: List<Emoji> = emptyList(),
var tabPreferences: List<TabData> = defaultTabs(),
var notificationsFilter: String = "[\"follow_request\"]",
var notificationsFilters: String = "[[\"follow_request\"], [\"reblog\",\"follow_request\",\"favourite\"]]",
var notificationsFilterIndex: Int = 0,
// Scope cannot be changed without re-login, so store it in case
// the scope needs to be changed in the future
var oauthScopes: String = "",
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
TimelineAccountEntity.class,
ConversationEntity.class
},
version = 51,
version = 52,
autoMigrations = {
@AutoMigration(from = 48, to = 49),
@AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class),
Expand Down Expand Up @@ -672,4 +672,12 @@ public void migrate(@NonNull SupportSQLiteDatabase database) {

@DeleteColumn(tableName = "AccountEntity", columnName = "activeNotifications")
static class MIGRATION_49_50 implements AutoMigrationSpec { }

public static final Migration MIGRATION_51_52 = new Migration(51, 52) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `AccountEntity` RENAME COLUMN `notificationFilter` TO `notificationFilters`");
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFilterIndex` INTEGER NOT NULL DEFAULT 0");
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,48 @@ import org.json.JSONArray
* Serialize to string array and deserialize notifications type
*/

fun serialize(data: Set<Notification.Type>?): String {
fun serialize(data: Array<Set<Notification.Type>>?): String {
val array = JSONArray()
data?.forEach {
array.put(it.presentation)
data?.forEach { innerArray ->
val filterArray = JSONArray()
innerArray.forEach {
filterArray.put(it.presentation)
}
array.put(filterArray)
}
return array.toString()
}

fun deserialize(data: String?): Set<Notification.Type> {
private fun deserializeInternal(array: JSONArray): Set<Notification.Type> {
val ret = HashSet<Notification.Type>()
for (i in 0 until array.length()) {
val item = array.getString(i)
val type = Notification.Type.byString(item)
if (type != Notification.Type.UNKNOWN) {
ret.add(type)
}
}
return ret
}

// This performs an implied conversion from AppDatabase 51 to 52.
private fun deserializeSingleFallback(array: JSONArray): Array<Set<Notification.Type>> {
val orig = deserializeInternal(array)
return arrayOf(orig, HashSet(orig))
}

fun deserialize(data: String?): Array<Set<Notification.Type>> {
val ret = mutableListOf<Set<Notification.Type>>()
data?.let {
val array = JSONArray(data)
for (i in 0 until array.length()) {
val item = array.getString(i)
val type = Notification.Type.byString(item)
if (type != Notification.Type.UNKNOWN) {
ret.add(type)
val filterArray = array.optJSONArray(i)
if (filterArray == null) {
return deserializeSingleFallback(array)
}

ret.add(deserializeInternal(filterArray))
}
}
return ret
return ret.toTypedArray()
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@
android:orientation="horizontal">

<Button
android:id="@+id/buttonClear"
android:id="@+id/buttonSwap"
style="@style/TuskyButton.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/notifications_clear"
android:text="@string/notifications_swap"
android:textSize="?attr/status_text_medium" />

<Button
Expand Down
8 changes: 4 additions & 4 deletions app/src/main/res/layout/fragment_timeline_notifications.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,21 @@
android:orientation="horizontal">

<Button
android:id="@+id/buttonClear"
android:id="@+id/buttonFilter"
style="@style/TuskyButton.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/notifications_clear"
android:text="@string/notifications_apply_filter"
android:textSize="?attr/status_text_medium" />

<Button
android:id="@+id/buttonFilter"
android:id="@+id/buttonSwap"
style="@style/TuskyButton.TextButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/notifications_apply_filter"
android:text="@string/notifications_swap"
android:textSize="?attr/status_text_medium" />

</LinearLayout>
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,9 @@
<string name="select_list_manage">Manage lists</string>
<string name="error_list_load">Error loading lists</string>
<string name="list">List</string>
<string name="notifications_swap">Swap</string>
<string name="notifications_swap_less">Less</string>
<string name="notifications_swap_more">More</string>
<string name="notifications_clear">Clear</string>
<string name="notifications_apply_filter">Filter</string>
<string name="filter_apply">Apply</string>
Expand Down