fix: Prevent potential crash when filters are slow to load (#205)
This previous code could crash if `filterModel.kind` (marked `lateinit`) had not been set before the filters are loaded. This could happen in rare cases. Fix this by rewriting `FilterModel`. Instead of creating a half-empty object that still needs further initialisation, delay the creation until all the necessary information is available, and pass it in the `FilterModel` constructor. This also forces code that uses `FilterModel` to properly handle the case where it might be null at the point where filtering decisions have to be made. This means that `TimelineViewModel` (and subclasses) no longer need the `init()` function to complete their construction, which was another significant code smell. Pass the `TimelineKind` to the view models via their `SavedStateHandle`. This showed that changing filters wasn't causing the timelines to update without a manual refresh, so fix that too. Editing filters sends change events for the old and new contexts (in case a context is removed from a filter), and deleting a filter sends a change event too.
This commit is contained in:
parent
34e37f9ebb
commit
523efa705c
|
@ -894,7 +894,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
||||
line="469"
|
||||
line="470"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -905,7 +905,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/accountlist/adapter/AccountAdapter.kt"
|
||||
line="80"
|
||||
line="81"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -916,7 +916,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
|
||||
line="43"
|
||||
line="44"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -927,7 +927,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
|
||||
line="49"
|
||||
line="50"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -938,7 +938,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/announcements/AnnouncementAdapter.kt"
|
||||
line="162"
|
||||
line="163"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -949,7 +949,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/accountlist/adapter/MutesAdapter.kt"
|
||||
line="107"
|
||||
line="108"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -960,7 +960,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/PollAdapter.kt"
|
||||
line="67"
|
||||
line="68"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -971,7 +971,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/PollAdapter.kt"
|
||||
line="67"
|
||||
line="68"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -982,7 +982,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/PollAdapter.kt"
|
||||
line="67"
|
||||
line="68"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -993,7 +993,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/PreviewPollOptionsAdapter.kt"
|
||||
line="35"
|
||||
line="36"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1004,7 +1004,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/TabAdapter.kt"
|
||||
line="54"
|
||||
line="55"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1015,7 +1015,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/TabAdapter.kt"
|
||||
line="157"
|
||||
line="158"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -1026,7 +1026,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/notifications/NotificationHelper.java"
|
||||
line="865"
|
||||
line="866"
|
||||
column="57"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2159,7 +2159,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
|
||||
line="34"
|
||||
line="35"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2170,7 +2170,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
|
||||
line="34"
|
||||
line="35"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2179,17 +2179,6 @@
|
|||
message="Access to `private` method `isSameDate` of class `Companion` requires synthetic accessor"
|
||||
errorLine1=" isSameDate(time, now, tz) -> sameDaySdf.format(time)"
|
||||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
|
||||
line="34"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SyntheticAccessor"
|
||||
message="Access to `private` method `isSameYear` of class `Companion` requires synthetic accessor"
|
||||
errorLine1=" isSameYear(time, now, tz) -> sameYearSdf.format(time)"
|
||||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
|
||||
line="35"
|
||||
|
@ -2203,7 +2192,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
|
||||
line="35"
|
||||
line="36"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2214,7 +2203,18 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
|
||||
line="35"
|
||||
line="36"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SyntheticAccessor"
|
||||
message="Access to `private` method `isSameYear` of class `Companion` requires synthetic accessor"
|
||||
errorLine1=" isSameYear(time, now, tz) -> sameYearSdf.format(time)"
|
||||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
|
||||
line="36"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2225,7 +2225,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
||||
line="357"
|
||||
line="358"
|
||||
column="29"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2236,7 +2236,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
||||
line="360"
|
||||
line="361"
|
||||
column="29"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2247,7 +2247,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
||||
line="366"
|
||||
line="367"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2258,7 +2258,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
||||
line="367"
|
||||
line="368"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2269,7 +2269,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
||||
line="369"
|
||||
line="370"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2280,7 +2280,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/account/AccountActivity.kt"
|
||||
line="379"
|
||||
line="380"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2291,7 +2291,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/accountlist/AccountListFragment.kt"
|
||||
line="137"
|
||||
line="138"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2302,7 +2302,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt"
|
||||
line="307"
|
||||
line="308"
|
||||
column="29"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2313,7 +2313,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt"
|
||||
line="313"
|
||||
line="314"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2467,7 +2467,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/compose/ComposeActivity.kt"
|
||||
line="536"
|
||||
line="537"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2478,7 +2478,7 @@
|
|||
errorLine2=" ~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/compose/ComposeActivity.kt"
|
||||
line="545"
|
||||
line="546"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2489,7 +2489,7 @@
|
|||
errorLine2=" ~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/compose/ComposeActivity.kt"
|
||||
line="545"
|
||||
line="546"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2500,7 +2500,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
|
||||
line="60"
|
||||
line="61"
|
||||
column="60"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2511,7 +2511,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
|
||||
line="61"
|
||||
line="62"
|
||||
column="60"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2522,7 +2522,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
|
||||
line="62"
|
||||
line="63"
|
||||
column="58"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2533,7 +2533,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
|
||||
line="122"
|
||||
line="123"
|
||||
column="37"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2544,7 +2544,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
|
||||
line="122"
|
||||
line="123"
|
||||
column="37"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2555,7 +2555,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/conversation/ConversationsFragment.kt"
|
||||
line="147"
|
||||
line="148"
|
||||
column="29"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2566,7 +2566,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/conversation/ConversationsFragment.kt"
|
||||
line="149"
|
||||
line="150"
|
||||
column="37"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2654,7 +2654,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/network/InstanceSwitchAuthInterceptor.kt"
|
||||
line="42"
|
||||
line="43"
|
||||
column="29"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2665,7 +2665,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/network/InstanceSwitchAuthInterceptor.kt"
|
||||
line="51"
|
||||
line="52"
|
||||
column="37"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2929,7 +2929,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/login/LoginActivity.kt"
|
||||
line="154"
|
||||
line="155"
|
||||
column="22"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2940,7 +2940,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
|
||||
line="142"
|
||||
line="143"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2951,7 +2951,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
|
||||
line="142"
|
||||
line="143"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2962,7 +2962,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
|
||||
line="151"
|
||||
line="152"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2973,7 +2973,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
|
||||
line="151"
|
||||
line="152"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2984,7 +2984,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
|
||||
line="172"
|
||||
line="173"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
|
@ -2995,7 +2995,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
|
||||
line="172"
|
||||
line="173"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3006,7 +3006,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
|
||||
line="175"
|
||||
line="176"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3017,7 +3017,7 @@
|
|||
errorLine2=" ~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
|
||||
line="175"
|
||||
line="176"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3457,7 +3457,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/compose/MediaPreviewAdapter.kt"
|
||||
line="138"
|
||||
line="139"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3688,7 +3688,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/fragment/SFragment.kt"
|
||||
line="182"
|
||||
line="183"
|
||||
column="48"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3732,7 +3732,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/report/adapter/StatusViewHolder.kt"
|
||||
line="59"
|
||||
line="60"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3743,7 +3743,7 @@
|
|||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/report/adapter/StatusViewHolder.kt"
|
||||
line="65"
|
||||
line="66"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3798,7 +3798,7 @@
|
|||
errorLine2=" ~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/components/timeline/TimelineFragment.kt"
|
||||
line="174"
|
||||
line="193"
|
||||
column="47"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3809,7 +3809,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/fragment/ViewImageFragment.kt"
|
||||
line="217"
|
||||
line="218"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3820,7 +3820,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/fragment/ViewImageFragment.kt"
|
||||
line="305"
|
||||
line="306"
|
||||
column="34"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3831,7 +3831,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/fragment/ViewImageFragment.kt"
|
||||
line="318"
|
||||
line="319"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3842,7 +3842,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/fragment/ViewImageFragment.kt"
|
||||
line="325"
|
||||
line="326"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3908,7 +3908,7 @@
|
|||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/fragment/ViewVideoFragment.kt"
|
||||
line="152"
|
||||
line="153"
|
||||
column="32"/>
|
||||
</issue>
|
||||
|
||||
|
@ -3919,7 +3919,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/fragment/ViewVideoFragment.kt"
|
||||
line="232"
|
||||
line="233"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
|
@ -5213,7 +5213,7 @@
|
|||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/app/pachli/util/ShareShortcutHelper.kt"
|
||||
line="86"
|
||||
line="87"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
|
@ -5224,7 +5224,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/TabAdapter.kt"
|
||||
line="91"
|
||||
line="92"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
|
@ -5235,7 +5235,7 @@
|
|||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/java/app/pachli/adapter/TabAdapter.kt"
|
||||
line="91"
|
||||
line="92"
|
||||
column="50"/>
|
||||
</issue>
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.pachli.appstore.EventHub
|
||||
import app.pachli.appstore.FilterChangedEvent
|
||||
import app.pachli.entity.Filter
|
||||
import app.pachli.entity.FilterKeyword
|
||||
import app.pachli.network.MastodonApi
|
||||
|
@ -88,9 +89,21 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
val action = action.value.action
|
||||
|
||||
return withContext(viewModelScope.coroutineContext) {
|
||||
originalFilter?.let { filter ->
|
||||
val success = originalFilter?.let { filter ->
|
||||
updateFilter(filter, title, contexts, action, durationIndex, context)
|
||||
} ?: createFilter(title, contexts, action, durationIndex, context)
|
||||
|
||||
// Send FilterChangedEvent for old and new contexts, to ensure that
|
||||
// e.g., removing a filter from "home" still notifies anything showing
|
||||
// the home timeline, so the timeline can be refreshed.
|
||||
if (success) {
|
||||
val originalKinds = originalFilter?.context?.map { Filter.Kind.from(it) } ?: emptyList()
|
||||
val newKinds = contexts.map { Filter.Kind.from(it) }
|
||||
(originalKinds + newKinds).distinct().forEach {
|
||||
eventHub.dispatch(FilterChangedEvent(it))
|
||||
}
|
||||
}
|
||||
return@withContext success
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -75,6 +75,9 @@ class FiltersViewModel @Inject constructor(
|
|||
api.deleteFilterV1(filter.id).fold(
|
||||
{
|
||||
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)
|
||||
filter.context.forEach {
|
||||
eventHub.dispatch(FilterChangedEvent(Filter.Kind.from(it)))
|
||||
}
|
||||
},
|
||||
{
|
||||
Snackbar.make(parent, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()
|
||||
|
|
|
@ -307,7 +307,6 @@ class NotificationsViewModel @Inject constructor(
|
|||
private val timelineCases: TimelineCases,
|
||||
private val eventHub: EventHub,
|
||||
private val filtersRepository: FiltersRepository,
|
||||
private val filterModel: FilterModel,
|
||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||
private val sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
) : ViewModel() {
|
||||
|
@ -349,9 +348,9 @@ class NotificationsViewModel @Inject constructor(
|
|||
viewModelScope.launch { uiAction.emit(action) }
|
||||
}
|
||||
|
||||
init {
|
||||
filterModel.kind = Filter.Kind.NOTIFICATIONS
|
||||
private var filterModel: FilterModel? = null
|
||||
|
||||
init {
|
||||
// Handle changes to notification filters
|
||||
val notificationFilter = uiAction
|
||||
.filterIsInstance<InfallibleUiAction.ApplyFilter>()
|
||||
|
@ -472,8 +471,11 @@ class NotificationsViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
eventHub.events
|
||||
.filterIsInstance<FilterChangedEvent>()
|
||||
.distinctUntilChanged()
|
||||
.map { getFilters() }
|
||||
.filter { it.filterKind == Filter.Kind.NOTIFICATIONS }
|
||||
.map {
|
||||
getFilters()
|
||||
repository.invalidate()
|
||||
}
|
||||
.onStart { getFilters() }
|
||||
.collect()
|
||||
}
|
||||
|
@ -516,7 +518,7 @@ class NotificationsViewModel @Inject constructor(
|
|||
return repository.getNotificationsStream(filter = filters, initialKey = initialKey)
|
||||
.map { pagingData ->
|
||||
pagingData.map { notification ->
|
||||
val filterAction = notification.status?.actionableStatus?.let { filterModel.shouldFilterStatus(it) } ?: Filter.Action.NONE
|
||||
val filterAction = notification.status?.actionableStatus?.let { filterModel?.filterActionFor(it) } ?: Filter.Action.NONE
|
||||
NotificationViewData.from(
|
||||
notification,
|
||||
isShowingContent = statusDisplayOptions.value.showSensitiveMedia ||
|
||||
|
@ -531,25 +533,12 @@ class NotificationsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current filters from the repository. Applies them locally if they are
|
||||
* v1 filters.
|
||||
*
|
||||
* Whatever the filter kind, the current timeline is invalidated, so it updates with the
|
||||
* most recent filters.
|
||||
*/
|
||||
/** Gets the current filters from the repository. */
|
||||
private fun getFilters() = viewModelScope.launch {
|
||||
try {
|
||||
when (val filters = filtersRepository.getFilters()) {
|
||||
is FilterKind.V1 -> {
|
||||
filterModel.initWithFilters(
|
||||
filters.filters.filter {
|
||||
it.context.contains("notifications")
|
||||
},
|
||||
)
|
||||
repository.invalidate()
|
||||
}
|
||||
is FilterKind.V2 -> repository.invalidate()
|
||||
filterModel = when (val filters = filtersRepository.getFilters()) {
|
||||
is FilterKind.V1 -> FilterModel(Filter.Kind.NOTIFICATIONS, filters.filters)
|
||||
is FilterKind.V2 -> FilterModel(Filter.Kind.NOTIFICATIONS)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
_uiErrorChannel.send(UiError.GetFilters(throwable))
|
||||
|
|
|
@ -29,9 +29,11 @@ import android.view.accessibility.AccessibilityManager
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.DEFAULT_ARGS_KEY
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewmodel.MutableCreationExtras
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -99,11 +101,30 @@ class TimelineFragment :
|
|||
RefreshableFragment,
|
||||
MenuProvider {
|
||||
|
||||
// Create the correct view model. Do this lazily because it depends on the value of
|
||||
// `timelineKind`, which won't be known until part way through `onCreate`. Pass this in
|
||||
// the "extras" to the view model, which are populated in to the `SavedStateHandle` it
|
||||
// takes as a parameter.
|
||||
//
|
||||
// If the navigation library was being used this would happen automatically, so this
|
||||
// workaround can be removed when that change happens.
|
||||
private val viewModel: TimelineViewModel by lazy {
|
||||
if (timelineKind == TimelineKind.Home) {
|
||||
viewModels<CachedTimelineViewModel>().value
|
||||
viewModels<CachedTimelineViewModel>(
|
||||
extrasProducer = {
|
||||
MutableCreationExtras(defaultViewModelCreationExtras).apply {
|
||||
set(DEFAULT_ARGS_KEY, TimelineViewModel.creationExtras(timelineKind))
|
||||
}
|
||||
},
|
||||
).value
|
||||
} else {
|
||||
viewModels<NetworkTimelineViewModel>().value
|
||||
viewModels<NetworkTimelineViewModel>(
|
||||
extrasProducer = {
|
||||
MutableCreationExtras(defaultViewModelCreationExtras).apply {
|
||||
set(DEFAULT_ARGS_KEY, TimelineViewModel.creationExtras(timelineKind))
|
||||
}
|
||||
},
|
||||
).value
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,8 +153,6 @@ class TimelineFragment :
|
|||
|
||||
timelineKind = arguments.getParcelable(KIND_ARG)!!
|
||||
|
||||
viewModel.init(timelineKind)
|
||||
|
||||
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
||||
|
||||
adapter = TimelinePagingAdapter(this, viewModel.statusDisplayOptions.value)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package app.pachli.components.timeline.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
|
@ -30,11 +31,9 @@ import app.pachli.appstore.PinEvent
|
|||
import app.pachli.appstore.ReblogEvent
|
||||
import app.pachli.components.timeline.CachedTimelineRepository
|
||||
import app.pachli.components.timeline.FiltersRepository
|
||||
import app.pachli.components.timeline.TimelineKind
|
||||
import app.pachli.db.AccountManager
|
||||
import app.pachli.entity.Filter
|
||||
import app.pachli.entity.Poll
|
||||
import app.pachli.network.FilterModel
|
||||
import app.pachli.usecase.TimelineCases
|
||||
import app.pachli.util.SharedPreferencesRepository
|
||||
import app.pachli.util.StatusDisplayOptionsRepository
|
||||
|
@ -51,8 +50,10 @@ import javax.inject.Inject
|
|||
/**
|
||||
* TimelineViewModel that caches all statuses in a local database
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltViewModel
|
||||
class CachedTimelineViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val repository: CachedTimelineRepository,
|
||||
timelineCases: TimelineCases,
|
||||
eventHub: EventHub,
|
||||
|
@ -60,39 +61,33 @@ class CachedTimelineViewModel @Inject constructor(
|
|||
accountManager: AccountManager,
|
||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||
sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
filterModel: FilterModel,
|
||||
private val gson: Gson,
|
||||
) : TimelineViewModel(
|
||||
savedStateHandle,
|
||||
timelineCases,
|
||||
eventHub,
|
||||
filtersRepository,
|
||||
accountManager,
|
||||
filterModel,
|
||||
statusDisplayOptionsRepository,
|
||||
sharedPreferencesRepository,
|
||||
) {
|
||||
|
||||
override lateinit var statuses: Flow<PagingData<StatusViewData>>
|
||||
override var statuses: Flow<PagingData<StatusViewData>>
|
||||
|
||||
init {
|
||||
readingPositionId = activeAccount.lastVisibleHomeTimelineStatusId
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun init(timelineKind: TimelineKind) {
|
||||
super.init(timelineKind)
|
||||
statuses = reload.flatMapLatest {
|
||||
getStatuses(timelineKind, initialKey = getInitialKey())
|
||||
getStatuses(initialKey = getInitialKey())
|
||||
}.cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
/** @return Flow of statuses that make up the timeline of [kind] */
|
||||
/** @return Flow of statuses that make up the timeline of [timelineKind] */
|
||||
private fun getStatuses(
|
||||
kind: TimelineKind,
|
||||
initialKey: String? = null,
|
||||
): Flow<PagingData<StatusViewData>> {
|
||||
Log.d(TAG, "getStatuses: kind: $kind, initialKey: $initialKey")
|
||||
return repository.getStatusStream(kind = kind, initialKey = initialKey)
|
||||
Log.d(TAG, "getStatuses: kind: $timelineKind, initialKey: $initialKey")
|
||||
return repository.getStatusStream(kind = timelineKind, initialKey = initialKey)
|
||||
.map { pagingData ->
|
||||
pagingData
|
||||
.map {
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package app.pachli.components.timeline.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
|
@ -30,11 +31,9 @@ import app.pachli.appstore.PinEvent
|
|||
import app.pachli.appstore.ReblogEvent
|
||||
import app.pachli.components.timeline.FiltersRepository
|
||||
import app.pachli.components.timeline.NetworkTimelineRepository
|
||||
import app.pachli.components.timeline.TimelineKind
|
||||
import app.pachli.db.AccountManager
|
||||
import app.pachli.entity.Filter
|
||||
import app.pachli.entity.Poll
|
||||
import app.pachli.network.FilterModel
|
||||
import app.pachli.usecase.TimelineCases
|
||||
import app.pachli.util.SharedPreferencesRepository
|
||||
import app.pachli.util.StatusDisplayOptionsRepository
|
||||
|
@ -50,8 +49,10 @@ import javax.inject.Inject
|
|||
/**
|
||||
* TimelineViewModel that caches all statuses in an in-memory list
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltViewModel
|
||||
class NetworkTimelineViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val repository: NetworkTimelineRepository,
|
||||
timelineCases: TimelineCases,
|
||||
eventHub: EventHub,
|
||||
|
@ -59,36 +60,32 @@ class NetworkTimelineViewModel @Inject constructor(
|
|||
accountManager: AccountManager,
|
||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||
sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
filterModel: FilterModel,
|
||||
) : TimelineViewModel(
|
||||
savedStateHandle,
|
||||
timelineCases,
|
||||
eventHub,
|
||||
filtersRepository,
|
||||
accountManager,
|
||||
filterModel,
|
||||
statusDisplayOptionsRepository,
|
||||
sharedPreferencesRepository,
|
||||
) {
|
||||
private val modifiedViewData = mutableMapOf<String, StatusViewData>()
|
||||
|
||||
override lateinit var statuses: Flow<PagingData<StatusViewData>>
|
||||
override var statuses: Flow<PagingData<StatusViewData>>
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun init(timelineKind: TimelineKind) {
|
||||
super.init(timelineKind)
|
||||
init {
|
||||
statuses = reload
|
||||
.flatMapLatest {
|
||||
getStatuses(timelineKind, initialKey = getInitialKey())
|
||||
getStatuses(initialKey = getInitialKey())
|
||||
}.cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
/** @return Flow of statuses that make up the timeline of [kind] */
|
||||
/** @return Flow of statuses that make up the timeline of [timelineKind] */
|
||||
private fun getStatuses(
|
||||
kind: TimelineKind,
|
||||
initialKey: String? = null,
|
||||
): Flow<PagingData<StatusViewData>> {
|
||||
Log.d(TAG, "getStatuses: kind: $kind, initialKey: $initialKey")
|
||||
return repository.getStatusStream(viewModelScope, kind = kind, initialKey = initialKey)
|
||||
Log.d(TAG, "getStatuses: kind: $timelineKind, initialKey: $initialKey")
|
||||
return repository.getStatusStream(viewModelScope, kind = timelineKind, initialKey = initialKey)
|
||||
.map { pagingData ->
|
||||
pagingData.map {
|
||||
modifiedViewData[it.id] ?: StatusViewData.from(
|
||||
|
|
|
@ -20,6 +20,9 @@ package app.pachli.components.timeline.viewmodel
|
|||
import android.util.Log
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
|
@ -253,11 +256,11 @@ sealed interface UiError {
|
|||
}
|
||||
|
||||
abstract class TimelineViewModel(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val timelineCases: TimelineCases,
|
||||
private val eventHub: EventHub,
|
||||
private val filtersRepository: FiltersRepository,
|
||||
protected val accountManager: AccountManager,
|
||||
private val filterModel: FilterModel,
|
||||
statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
|
||||
private val sharedPreferencesRepository: SharedPreferencesRepository,
|
||||
) : ViewModel() {
|
||||
|
@ -296,8 +299,7 @@ abstract class TimelineViewModel(
|
|||
viewModelScope.launch { uiAction.emit(action) }
|
||||
}
|
||||
|
||||
var timelineKind: TimelineKind = TimelineKind.Home
|
||||
private set
|
||||
val timelineKind: TimelineKind = savedStateHandle.get<TimelineKind>(TIMELINE_KIND_TAG)!!
|
||||
|
||||
private var filterRemoveReplies = false
|
||||
private var filterRemoveReblogs = false
|
||||
|
@ -311,6 +313,8 @@ abstract class TimelineViewModel(
|
|||
open var readingPositionId: String? = null
|
||||
protected set
|
||||
|
||||
private var filterModel: FilterModel? = null
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
updateFiltersFromPreferences().collectLatest {
|
||||
|
@ -384,13 +388,6 @@ abstract class TimelineViewModel(
|
|||
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
|
||||
initialValue = UiState(showFabWhileScrolling = true),
|
||||
)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
open fun init(timelineKind: TimelineKind) {
|
||||
this.timelineKind = timelineKind
|
||||
|
||||
filterModel.kind = Filter.Kind.from(timelineKind)
|
||||
|
||||
if (timelineKind is TimelineKind.Home) {
|
||||
// Note the variable is "true if filter" but the underlying preference/settings text is "true if show"
|
||||
|
@ -501,43 +498,30 @@ abstract class TimelineViewModel(
|
|||
) {
|
||||
return Filter.Action.HIDE
|
||||
} else {
|
||||
statusViewData.filterAction = filterModel.shouldFilterStatus(status.actionableStatus)
|
||||
statusViewData.filterAction = filterModel?.filterActionFor(status.actionableStatus) ?: Filter.Action.NONE
|
||||
statusViewData.filterAction
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the current set of filters if filter-related preferences change */
|
||||
// TODO: https://github.com/tuskyapp/Tusky/issues/3546, and update if a v2 filter is
|
||||
// updated as well.
|
||||
private fun updateFiltersFromPreferences() = eventHub.events
|
||||
.filterIsInstance<FilterChangedEvent>()
|
||||
.filter { filterContextMatchesKind(timelineKind, listOf(it.filterKind)) }
|
||||
.distinctUntilChanged()
|
||||
.map { getFilters() }
|
||||
.map {
|
||||
getFilters()
|
||||
reloadKeepingReadingPosition()
|
||||
}
|
||||
.onStart { getFilters() }
|
||||
|
||||
/**
|
||||
* Gets the current filters from the repository. Applies them locally if they are
|
||||
* v1 filters.
|
||||
*
|
||||
* Whatever the filter kind, the current timeline is invalidated, so it updates with the
|
||||
* most recent filters.
|
||||
*/
|
||||
/** Gets the current filters from the repository. */
|
||||
private fun getFilters() {
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "getFilters()")
|
||||
try {
|
||||
when (val filters = filtersRepository.getFilters()) {
|
||||
is FilterKind.V1 -> {
|
||||
filterModel.initWithFilters(
|
||||
filters.filters.filter {
|
||||
filterContextMatchesString(timelineKind, it.context)
|
||||
},
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
is FilterKind.V2 -> invalidate()
|
||||
val filterKind = Filter.Kind.from(timelineKind)
|
||||
filterModel = when (val filters = filtersRepository.getFilters()) {
|
||||
is FilterKind.V1 -> FilterModel(filterKind, filters.filters)
|
||||
is FilterKind.V2 -> FilterModel(filterKind)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
Log.d(TAG, "updateFilter(): Error fetching filters: ${throwable.message}")
|
||||
|
@ -619,12 +603,14 @@ abstract class TimelineViewModel(
|
|||
private const val TAG = "TimelineViewModel"
|
||||
private val THROTTLE_TIMEOUT = 500.milliseconds
|
||||
|
||||
fun filterContextMatchesString(
|
||||
timelineKind: TimelineKind,
|
||||
filterContext: List<String>,
|
||||
): Boolean {
|
||||
return filterContext.contains(Filter.Kind.from(timelineKind).kind)
|
||||
}
|
||||
/** Tag for the timelineKind in `savedStateHandle` */
|
||||
@VisibleForTesting(VisibleForTesting.PRIVATE)
|
||||
const val TIMELINE_KIND_TAG = "timelineKind"
|
||||
|
||||
/** Create extras for this view model */
|
||||
fun creationExtras(timelineKind: TimelineKind) = bundleOf(
|
||||
TIMELINE_KIND_TAG to timelineKind,
|
||||
)
|
||||
|
||||
fun filterContextMatchesKind(
|
||||
timelineKind: TimelineKind,
|
||||
|
|
|
@ -23,6 +23,7 @@ import app.pachli.appstore.BlockEvent
|
|||
import app.pachli.appstore.BookmarkEvent
|
||||
import app.pachli.appstore.EventHub
|
||||
import app.pachli.appstore.FavoriteEvent
|
||||
import app.pachli.appstore.FilterChangedEvent
|
||||
import app.pachli.appstore.PinEvent
|
||||
import app.pachli.appstore.ReblogEvent
|
||||
import app.pachli.appstore.StatusComposedEvent
|
||||
|
@ -36,7 +37,6 @@ import app.pachli.db.AccountEntity
|
|||
import app.pachli.db.AccountManager
|
||||
import app.pachli.db.TimelineDao
|
||||
import app.pachli.entity.Filter
|
||||
import app.pachli.entity.FilterV1
|
||||
import app.pachli.entity.Status
|
||||
import app.pachli.network.FilterModel
|
||||
import app.pachli.network.MastodonApi
|
||||
|
@ -61,7 +61,6 @@ import javax.inject.Inject
|
|||
@HiltViewModel
|
||||
class ViewThreadViewModel @Inject constructor(
|
||||
private val api: MastodonApi,
|
||||
private val filterModel: FilterModel,
|
||||
private val timelineCases: TimelineCases,
|
||||
eventHub: EventHub,
|
||||
accountManager: AccountManager,
|
||||
|
@ -89,6 +88,8 @@ class ViewThreadViewModel @Inject constructor(
|
|||
|
||||
val activeAccount: AccountEntity
|
||||
|
||||
private var filterModel: FilterModel? = null
|
||||
|
||||
init {
|
||||
activeAccount = accountManager.activeAccount!!
|
||||
alwaysShowSensitiveMedia = activeAccount.alwaysShowSensitiveMedia
|
||||
|
@ -106,6 +107,11 @@ class ViewThreadViewModel @Inject constructor(
|
|||
is StatusComposedEvent -> handleStatusComposedEvent(event)
|
||||
is StatusDeletedEvent -> handleStatusDeletedEvent(event)
|
||||
is StatusEditedEvent -> handleStatusEditedEvent(event)
|
||||
is FilterChangedEvent -> {
|
||||
if (event.filterKind == Filter.Kind.THREAD) {
|
||||
loadFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -474,16 +480,9 @@ class ViewThreadViewModel @Inject constructor(
|
|||
private fun loadFilters() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
when (val filters = filtersRepository.getFilters()) {
|
||||
is FilterKind.V1 -> {
|
||||
filterModel.initWithFilters(
|
||||
filters.filters.filter { filter ->
|
||||
filter.context.contains(FilterV1.THREAD)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is FilterKind.V2 -> filterModel.kind = Filter.Kind.THREAD
|
||||
filterModel = when (val filters = filtersRepository.getFilters()) {
|
||||
is FilterKind.V1 -> FilterModel(Filter.Kind.THREAD, filters.filters)
|
||||
is FilterKind.V2 -> FilterModel(Filter.Kind.THREAD)
|
||||
}
|
||||
updateStatuses()
|
||||
} catch (_: Exception) {
|
||||
|
@ -509,7 +508,7 @@ class ViewThreadViewModel @Inject constructor(
|
|||
if (status.isDetailed) {
|
||||
true
|
||||
} else {
|
||||
status.filterAction = filterModel.shouldFilterStatus(status.status)
|
||||
status.filterAction = filterModel?.filterActionFor(status.status) ?: Filter.Action.NONE
|
||||
status.filterAction != Filter.Action.HIDE
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,28 +6,28 @@ import app.pachli.entity.Status
|
|||
import app.pachli.util.parseAsMastodonHtml
|
||||
import java.util.Date
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* One-stop for status filtering logic using Mastodon's filters.
|
||||
* Filter statuses using V1 or V2 filters.
|
||||
*
|
||||
* 1. You init with [initWithFilters], this compiles regex pattern.
|
||||
* 2. You call [shouldFilterStatus] to figure out what to display when you load statuses.
|
||||
* Construct with [filterKind] that corresponds to the kind of timeline, and optionally the set
|
||||
* of v1 filters that should be applied.
|
||||
*/
|
||||
class FilterModel @Inject constructor() {
|
||||
class FilterModel constructor(private val filterKind: Filter.Kind, v1filters: List<FilterV1>? = null) {
|
||||
/** Pattern to use when matching v1 filters against a status. Null if these are v2 filters */
|
||||
private var pattern: Pattern? = null
|
||||
private var v1 = false
|
||||
lateinit var kind: Filter.Kind
|
||||
|
||||
fun initWithFilters(filters: List<FilterV1>) {
|
||||
v1 = true
|
||||
this.pattern = makeFilter(filters)
|
||||
init {
|
||||
pattern = v1filters?.let { list ->
|
||||
makeFilter(list.filter { it.context.contains(filterKind.kind) })
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldFilterStatus(status: Status): Filter.Action {
|
||||
if (v1) {
|
||||
/** @return the [Filter.Action] that should be applied to this status */
|
||||
fun filterActionFor(status: Status): Filter.Action {
|
||||
pattern?.let { pat ->
|
||||
// Patterns are expensive and thread-safe, matchers are neither.
|
||||
val matcher = pattern?.matcher("") ?: return Filter.Action.NONE
|
||||
val matcher = pat.matcher("") ?: return Filter.Action.NONE
|
||||
|
||||
if (status.poll?.options?.any { matcher.reset(it.title).find() } == true) {
|
||||
return Filter.Action.HIDE
|
||||
|
@ -48,7 +48,7 @@ class FilterModel @Inject constructor() {
|
|||
}
|
||||
|
||||
val matchingKind = status.filtered?.filter { result ->
|
||||
result.filter.kinds.contains(kind)
|
||||
result.filter.kinds.contains(filterKind)
|
||||
}
|
||||
|
||||
return if (matchingKind.isNullOrEmpty()) {
|
||||
|
|
|
@ -41,7 +41,6 @@ class FilterV1Test {
|
|||
|
||||
@Before
|
||||
fun setup() {
|
||||
filterModel = FilterModel()
|
||||
val filters = listOf(
|
||||
FilterV1(
|
||||
id = "123",
|
||||
|
@ -101,14 +100,14 @@ class FilterV1Test {
|
|||
),
|
||||
)
|
||||
|
||||
filterModel.initWithFilters(filters)
|
||||
filterModel = FilterModel(Filter.Kind.HOME, filters)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldNotFilter() {
|
||||
assertEquals(
|
||||
Filter.Action.NONE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "should not be filtered"),
|
||||
),
|
||||
)
|
||||
|
@ -118,7 +117,7 @@ class FilterV1Test {
|
|||
fun shouldFilter_whenContentMatchesBadWord() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "one two badWord three"),
|
||||
),
|
||||
)
|
||||
|
@ -128,7 +127,7 @@ class FilterV1Test {
|
|||
fun shouldFilter_whenContentMatchesBadWordPart() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "one two badWordPart three"),
|
||||
),
|
||||
)
|
||||
|
@ -138,7 +137,7 @@ class FilterV1Test {
|
|||
fun shouldFilter_whenContentMatchesBadWholeWord() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "one two badWholeWord three"),
|
||||
),
|
||||
)
|
||||
|
@ -148,7 +147,7 @@ class FilterV1Test {
|
|||
fun shouldNotFilter_whenContentDoesNotMatchWholeWord() {
|
||||
assertEquals(
|
||||
Filter.Action.NONE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "one two badWholeWordTest three"),
|
||||
),
|
||||
)
|
||||
|
@ -158,7 +157,7 @@ class FilterV1Test {
|
|||
fun shouldFilter_whenSpoilerTextDoesMatch() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(
|
||||
content = "should not be filtered",
|
||||
spoilerText = "badWord should be filtered",
|
||||
|
@ -171,7 +170,7 @@ class FilterV1Test {
|
|||
fun shouldFilter_whenPollTextDoesMatch() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(
|
||||
content = "should not be filtered",
|
||||
spoilerText = "should not be filtered",
|
||||
|
@ -185,7 +184,7 @@ class FilterV1Test {
|
|||
fun shouldFilter_whenMediaDescriptionDoesMatch() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(
|
||||
content = "should not be filtered",
|
||||
spoilerText = "should not be filtered",
|
||||
|
@ -199,7 +198,7 @@ class FilterV1Test {
|
|||
fun shouldFilterPartialWord_whenWholeWordFilterContainsNonAlphanumericCharacters() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "one two someone@twitter.com three"),
|
||||
),
|
||||
)
|
||||
|
@ -209,7 +208,7 @@ class FilterV1Test {
|
|||
fun shouldFilterHashtags() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "#hashtag one two three"),
|
||||
),
|
||||
)
|
||||
|
@ -219,7 +218,7 @@ class FilterV1Test {
|
|||
fun shouldFilterHashtags_whenContentIsMarkedUp() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "<p><a href=\"https://foo.bar/tags/hashtag\" class=\"mention hashtag\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">#<span>hashtag</span></a>one two three</p>"),
|
||||
),
|
||||
)
|
||||
|
@ -229,7 +228,7 @@ class FilterV1Test {
|
|||
fun shouldNotFilterHtmlAttributes() {
|
||||
assertEquals(
|
||||
Filter.Action.NONE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "<p><a href=\"https://foo.bar/\">https://foo.bar/</a> one two three</p>"),
|
||||
),
|
||||
)
|
||||
|
@ -239,7 +238,7 @@ class FilterV1Test {
|
|||
fun shouldNotFilter_whenFilterIsExpired() {
|
||||
assertEquals(
|
||||
Filter.Action.NONE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "content matching expired filter should not be filtered"),
|
||||
),
|
||||
)
|
||||
|
@ -249,7 +248,7 @@ class FilterV1Test {
|
|||
fun shouldFilter_whenFilterIsUnexpired() {
|
||||
assertEquals(
|
||||
Filter.Action.HIDE,
|
||||
filterModel.shouldFilterStatus(
|
||||
filterModel.filterActionFor(
|
||||
mockStatus(content = "content matching unexpired filter should be filtered"),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -19,12 +19,12 @@ package app.pachli.components.notifications
|
|||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.pachli.appstore.EventHub
|
||||
import app.pachli.components.timeline.FilterKind
|
||||
import app.pachli.components.timeline.FiltersRepository
|
||||
import app.pachli.components.timeline.MainCoroutineRule
|
||||
import app.pachli.db.AccountEntity
|
||||
import app.pachli.db.AccountManager
|
||||
import app.pachli.fakes.InMemorySharedPreferences
|
||||
import app.pachli.network.FilterModel
|
||||
import app.pachli.settings.AccountPreferenceDataStore
|
||||
import app.pachli.usecase.TimelineCases
|
||||
import app.pachli.util.SharedPreferencesRepository
|
||||
|
@ -51,7 +51,6 @@ abstract class NotificationsViewModelTestBase {
|
|||
protected lateinit var viewModel: NotificationsViewModel
|
||||
private lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository
|
||||
private lateinit var filtersRepository: FiltersRepository
|
||||
private lateinit var filterModel: FilterModel
|
||||
|
||||
private val eventHub = EventHub()
|
||||
|
||||
|
@ -96,8 +95,9 @@ abstract class NotificationsViewModelTestBase {
|
|||
)
|
||||
|
||||
timelineCases = mock()
|
||||
filtersRepository = mock()
|
||||
filterModel = mock()
|
||||
filtersRepository = mock {
|
||||
onBlocking { getFilters() } doReturn FilterKind.V2(emptyList())
|
||||
}
|
||||
|
||||
sharedPreferencesRepository = SharedPreferencesRepository(
|
||||
InMemorySharedPreferences(),
|
||||
|
@ -117,7 +117,6 @@ abstract class NotificationsViewModelTestBase {
|
|||
timelineCases,
|
||||
eventHub,
|
||||
filtersRepository,
|
||||
filterModel,
|
||||
statusDisplayOptionsRepository,
|
||||
sharedPreferencesRepository,
|
||||
)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package app.pachli.components.timeline
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.pachli.PachliApplication
|
||||
import app.pachli.appstore.EventHub
|
||||
|
@ -24,7 +25,6 @@ import app.pachli.components.timeline.viewmodel.CachedTimelineViewModel
|
|||
import app.pachli.components.timeline.viewmodel.TimelineViewModel
|
||||
import app.pachli.db.AccountManager
|
||||
import app.pachli.entity.Account
|
||||
import app.pachli.network.FilterModel
|
||||
import app.pachli.network.MastodonApi
|
||||
import app.pachli.settings.AccountPreferenceDataStore
|
||||
import app.pachli.usecase.TimelineCases
|
||||
|
@ -76,19 +76,19 @@ abstract class CachedTimelineViewModelTestBase {
|
|||
@Inject
|
||||
lateinit var sharedPreferencesRepository: SharedPreferencesRepository
|
||||
|
||||
private lateinit var cachedTimelineRepository: CachedTimelineRepository
|
||||
@Inject
|
||||
lateinit var filtersRepository: FiltersRepository
|
||||
|
||||
@Inject
|
||||
lateinit var cachedTimelineRepository: CachedTimelineRepository
|
||||
|
||||
private lateinit var accountPreferenceDataStore: AccountPreferenceDataStore
|
||||
protected lateinit var timelineCases: TimelineCases
|
||||
private lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository
|
||||
private lateinit var filtersRepository: FiltersRepository
|
||||
private lateinit var filterModel: FilterModel
|
||||
protected lateinit var viewModel: TimelineViewModel
|
||||
|
||||
private val eventHub = EventHub()
|
||||
|
||||
/** Empty success response, for API calls that return one */
|
||||
protected var emptySuccess = Response.success("".toResponseBody())
|
||||
|
||||
/** Empty error response, for API calls that return one */
|
||||
private var emptyError: Response<ResponseBody> = Response.error(404, "".toResponseBody())
|
||||
|
||||
|
@ -102,6 +102,7 @@ abstract class CachedTimelineViewModelTestBase {
|
|||
reset(mastodonApi)
|
||||
mastodonApi.stub {
|
||||
onBlocking { getCustomEmojis() } doReturn NetworkResult.failure(Exception())
|
||||
onBlocking { getFilters() } doReturn NetworkResult.success(emptyList())
|
||||
}
|
||||
|
||||
accountManager.addAccount(
|
||||
|
@ -123,16 +124,12 @@ abstract class CachedTimelineViewModelTestBase {
|
|||
),
|
||||
)
|
||||
|
||||
cachedTimelineRepository = mock()
|
||||
|
||||
accountPreferenceDataStore = AccountPreferenceDataStore(
|
||||
accountManager,
|
||||
TestScope(),
|
||||
)
|
||||
|
||||
timelineCases = mock()
|
||||
filtersRepository = mock()
|
||||
filterModel = mock()
|
||||
|
||||
statusDisplayOptionsRepository = StatusDisplayOptionsRepository(
|
||||
sharedPreferencesRepository,
|
||||
|
@ -142,6 +139,7 @@ abstract class CachedTimelineViewModelTestBase {
|
|||
)
|
||||
|
||||
viewModel = CachedTimelineViewModel(
|
||||
SavedStateHandle(mapOf(TimelineViewModel.TIMELINE_KIND_TAG to TimelineKind.Home)),
|
||||
cachedTimelineRepository,
|
||||
timelineCases,
|
||||
eventHub,
|
||||
|
@ -149,11 +147,7 @@ abstract class CachedTimelineViewModelTestBase {
|
|||
accountManager,
|
||||
statusDisplayOptionsRepository,
|
||||
sharedPreferencesRepository,
|
||||
filterModel,
|
||||
Gson(),
|
||||
)
|
||||
|
||||
// Initialisation with the Home timeline
|
||||
viewModel.init(TimelineKind.Home)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,17 +17,19 @@
|
|||
|
||||
package app.pachli.components.timeline
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import app.pachli.appstore.EventHub
|
||||
import app.pachli.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||
import app.pachli.components.timeline.viewmodel.TimelineViewModel
|
||||
import app.pachli.db.AccountManager
|
||||
import app.pachli.entity.Account
|
||||
import app.pachli.network.FilterModel
|
||||
import app.pachli.network.MastodonApi
|
||||
import app.pachli.settings.AccountPreferenceDataStore
|
||||
import app.pachli.usecase.TimelineCases
|
||||
import app.pachli.util.SharedPreferencesRepository
|
||||
import app.pachli.util.StatusDisplayOptionsRepository
|
||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
@ -36,7 +38,10 @@ import okhttp3.ResponseBody.Companion.toResponseBody
|
|||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.reset
|
||||
import org.mockito.kotlin.stub
|
||||
import org.robolectric.annotation.Config
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
@ -57,22 +62,25 @@ abstract class NetworkTimelineViewModelTestBase {
|
|||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
@Inject
|
||||
lateinit var mastodonApi: MastodonApi
|
||||
|
||||
@Inject
|
||||
lateinit var sharedPreferencesRepository: SharedPreferencesRepository
|
||||
|
||||
private lateinit var networkTimelineRepository: NetworkTimelineRepository
|
||||
@Inject
|
||||
lateinit var filtersRepository: FiltersRepository
|
||||
|
||||
@Inject
|
||||
lateinit var networkTimelineRepository: NetworkTimelineRepository
|
||||
|
||||
private lateinit var accountPreferenceDataStore: AccountPreferenceDataStore
|
||||
protected lateinit var timelineCases: TimelineCases
|
||||
private lateinit var filtersRepository: FiltersRepository
|
||||
private lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository
|
||||
private lateinit var filterModel: FilterModel
|
||||
protected lateinit var viewModel: TimelineViewModel
|
||||
|
||||
private val eventHub = EventHub()
|
||||
|
||||
/** Empty success response, for API calls that return one */
|
||||
protected var emptySuccess = Response.success("".toResponseBody())
|
||||
|
||||
/** Empty error response, for API calls that return one */
|
||||
private var emptyError: Response<ResponseBody> = Response.error(404, "".toResponseBody())
|
||||
|
||||
|
@ -83,6 +91,12 @@ abstract class NetworkTimelineViewModelTestBase {
|
|||
fun setup() {
|
||||
hilt.inject()
|
||||
|
||||
reset(mastodonApi)
|
||||
mastodonApi.stub {
|
||||
onBlocking { getCustomEmojis() } doReturn NetworkResult.failure(Exception())
|
||||
onBlocking { getFilters() } doReturn NetworkResult.success(emptyList())
|
||||
}
|
||||
|
||||
accountManager.addAccount(
|
||||
accessToken = "token",
|
||||
domain = "domain.example",
|
||||
|
@ -102,16 +116,12 @@ abstract class NetworkTimelineViewModelTestBase {
|
|||
),
|
||||
)
|
||||
|
||||
networkTimelineRepository = mock()
|
||||
|
||||
accountPreferenceDataStore = AccountPreferenceDataStore(
|
||||
accountManager,
|
||||
TestScope(),
|
||||
)
|
||||
|
||||
timelineCases = mock()
|
||||
filtersRepository = mock()
|
||||
filterModel = mock()
|
||||
|
||||
statusDisplayOptionsRepository = StatusDisplayOptionsRepository(
|
||||
sharedPreferencesRepository,
|
||||
|
@ -121,6 +131,7 @@ abstract class NetworkTimelineViewModelTestBase {
|
|||
)
|
||||
|
||||
viewModel = NetworkTimelineViewModel(
|
||||
SavedStateHandle(mapOf(TimelineViewModel.TIMELINE_KIND_TAG to TimelineKind.Bookmarks)),
|
||||
networkTimelineRepository,
|
||||
timelineCases,
|
||||
eventHub,
|
||||
|
@ -128,11 +139,6 @@ abstract class NetworkTimelineViewModelTestBase {
|
|||
accountManager,
|
||||
statusDisplayOptionsRepository,
|
||||
sharedPreferencesRepository,
|
||||
filterModel,
|
||||
)
|
||||
|
||||
// Initialisation with any timeline kind, as long as it's not Home
|
||||
// (Home uses CachedTimelineViewModel)
|
||||
viewModel.init(TimelineKind.Bookmarks)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import app.pachli.db.AccountManager
|
|||
import app.pachli.db.TimelineDao
|
||||
import app.pachli.entity.Account
|
||||
import app.pachli.entity.StatusContext
|
||||
import app.pachli.network.FilterModel
|
||||
import app.pachli.network.MastodonApi
|
||||
import app.pachli.settings.AccountPreferenceDataStore
|
||||
import app.pachli.usecase.TimelineCases
|
||||
|
@ -130,8 +129,6 @@ class ViewThreadViewModelTest {
|
|||
onBlocking { getFilters() } doReturn FilterKind.V2(emptyList())
|
||||
}
|
||||
|
||||
val filterModel = FilterModel()
|
||||
|
||||
val defaultAccount = AccountEntity(
|
||||
id = 1,
|
||||
domain = "mastodon.test",
|
||||
|
@ -178,7 +175,6 @@ class ViewThreadViewModelTest {
|
|||
|
||||
viewModel = ViewThreadViewModel(
|
||||
mastodonApi,
|
||||
filterModel,
|
||||
timelineCases,
|
||||
eventHub,
|
||||
accountManager,
|
||||
|
|
Loading…
Reference in New Issue