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:
Nik Clayton 2023-10-28 19:54:46 +02:00 committed by GitHub
parent 34e37f9ebb
commit 523efa705c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 244 additions and 249 deletions

View File

@ -894,7 +894,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/account/AccountActivity.kt" file="src/main/java/app/pachli/components/account/AccountActivity.kt"
line="469" line="470"
column="9"/> column="9"/>
</issue> </issue>
@ -905,7 +905,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/accountlist/adapter/AccountAdapter.kt" file="src/main/java/app/pachli/components/accountlist/adapter/AccountAdapter.kt"
line="80" line="81"
column="9"/> column="9"/>
</issue> </issue>
@ -916,7 +916,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt" file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
line="43" line="44"
column="9"/> column="9"/>
</issue> </issue>
@ -927,7 +927,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt" file="src/main/java/app/pachli/adapter/AccountFieldEditAdapter.kt"
line="49" line="50"
column="9"/> column="9"/>
</issue> </issue>
@ -938,7 +938,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/announcements/AnnouncementAdapter.kt" file="src/main/java/app/pachli/components/announcements/AnnouncementAdapter.kt"
line="162" line="163"
column="9"/> column="9"/>
</issue> </issue>
@ -949,7 +949,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/accountlist/adapter/MutesAdapter.kt" file="src/main/java/app/pachli/components/accountlist/adapter/MutesAdapter.kt"
line="107" line="108"
column="9"/> column="9"/>
</issue> </issue>
@ -960,7 +960,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/adapter/PollAdapter.kt" file="src/main/java/app/pachli/adapter/PollAdapter.kt"
line="67" line="68"
column="9"/> column="9"/>
</issue> </issue>
@ -971,7 +971,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/adapter/PollAdapter.kt" file="src/main/java/app/pachli/adapter/PollAdapter.kt"
line="67" line="68"
column="9"/> column="9"/>
</issue> </issue>
@ -982,7 +982,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/adapter/PollAdapter.kt" file="src/main/java/app/pachli/adapter/PollAdapter.kt"
line="67" line="68"
column="9"/> column="9"/>
</issue> </issue>
@ -993,7 +993,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/adapter/PreviewPollOptionsAdapter.kt" file="src/main/java/app/pachli/adapter/PreviewPollOptionsAdapter.kt"
line="35" line="36"
column="9"/> column="9"/>
</issue> </issue>
@ -1004,7 +1004,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/adapter/TabAdapter.kt" file="src/main/java/app/pachli/adapter/TabAdapter.kt"
line="54" line="55"
column="9"/> column="9"/>
</issue> </issue>
@ -1015,7 +1015,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/adapter/TabAdapter.kt" file="src/main/java/app/pachli/adapter/TabAdapter.kt"
line="157" line="158"
column="13"/> column="13"/>
</issue> </issue>
@ -1026,7 +1026,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/notifications/NotificationHelper.java" file="src/main/java/app/pachli/components/notifications/NotificationHelper.java"
line="865" line="866"
column="57"/> column="57"/>
</issue> </issue>
@ -2159,7 +2159,7 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt" file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
line="34" line="35"
column="13"/> column="13"/>
</issue> </issue>
@ -2170,7 +2170,7 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt" file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
line="34" line="35"
column="13"/> column="13"/>
</issue> </issue>
@ -2179,17 +2179,6 @@
message="Access to `private` method `isSameDate` of class `Companion` requires synthetic accessor" message="Access to `private` method `isSameDate` of class `Companion` requires synthetic accessor"
errorLine1=" isSameDate(time, now, tz) -> sameDaySdf.format(time)" errorLine1=" isSameDate(time, now, tz) -> sameDaySdf.format(time)"
errorLine2=" ~~~~~~~~~~"> 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 <location
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt" file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
line="35" line="35"
@ -2203,7 +2192,7 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt" file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt"
line="35" line="36"
column="13"/> column="13"/>
</issue> </issue>
@ -2214,7 +2203,18 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/util/AbsoluteTimeFormatter.kt" 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"/> column="13"/>
</issue> </issue>
@ -2225,7 +2225,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/account/AccountActivity.kt" file="src/main/java/app/pachli/components/account/AccountActivity.kt"
line="357" line="358"
column="29"/> column="29"/>
</issue> </issue>
@ -2236,7 +2236,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/account/AccountActivity.kt" file="src/main/java/app/pachli/components/account/AccountActivity.kt"
line="360" line="361"
column="29"/> column="29"/>
</issue> </issue>
@ -2247,7 +2247,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/account/AccountActivity.kt" file="src/main/java/app/pachli/components/account/AccountActivity.kt"
line="366" line="367"
column="21"/> column="21"/>
</issue> </issue>
@ -2258,7 +2258,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/account/AccountActivity.kt" file="src/main/java/app/pachli/components/account/AccountActivity.kt"
line="367" line="368"
column="21"/> column="21"/>
</issue> </issue>
@ -2269,7 +2269,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/account/AccountActivity.kt" file="src/main/java/app/pachli/components/account/AccountActivity.kt"
line="369" line="370"
column="21"/> column="21"/>
</issue> </issue>
@ -2280,7 +2280,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/account/AccountActivity.kt" file="src/main/java/app/pachli/components/account/AccountActivity.kt"
line="379" line="380"
column="21"/> column="21"/>
</issue> </issue>
@ -2291,7 +2291,7 @@
errorLine2=" ~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/accountlist/AccountListFragment.kt" file="src/main/java/app/pachli/components/accountlist/AccountListFragment.kt"
line="137" line="138"
column="17"/> column="17"/>
</issue> </issue>
@ -2302,7 +2302,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt" file="src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt"
line="307" line="308"
column="29"/> column="29"/>
</issue> </issue>
@ -2313,7 +2313,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt" file="src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt"
line="313" line="314"
column="25"/> column="25"/>
</issue> </issue>
@ -2467,7 +2467,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/compose/ComposeActivity.kt" file="src/main/java/app/pachli/components/compose/ComposeActivity.kt"
line="536" line="537"
column="21"/> column="21"/>
</issue> </issue>
@ -2478,7 +2478,7 @@
errorLine2=" ~~~~~~~~~"> errorLine2=" ~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/compose/ComposeActivity.kt" file="src/main/java/app/pachli/components/compose/ComposeActivity.kt"
line="545" line="546"
column="17"/> column="17"/>
</issue> </issue>
@ -2489,7 +2489,7 @@
errorLine2=" ~~~~~~~~~"> errorLine2=" ~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/compose/ComposeActivity.kt" file="src/main/java/app/pachli/components/compose/ComposeActivity.kt"
line="545" line="546"
column="17"/> column="17"/>
</issue> </issue>
@ -2500,7 +2500,7 @@
errorLine2=" ~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt" file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
line="60" line="61"
column="60"/> column="60"/>
</issue> </issue>
@ -2511,7 +2511,7 @@
errorLine2=" ~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt" file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
line="61" line="62"
column="60"/> column="60"/>
</issue> </issue>
@ -2522,7 +2522,7 @@
errorLine2=" ~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt" file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
line="62" line="63"
column="58"/> column="58"/>
</issue> </issue>
@ -2533,7 +2533,7 @@
errorLine2=" ~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt" file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
line="122" line="123"
column="37"/> column="37"/>
</issue> </issue>
@ -2544,7 +2544,7 @@
errorLine2=" ~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt" file="src/main/java/app/pachli/components/compose/ComposeAutoCompleteAdapter.kt"
line="122" line="123"
column="37"/> column="37"/>
</issue> </issue>
@ -2555,7 +2555,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/conversation/ConversationsFragment.kt" file="src/main/java/app/pachli/components/conversation/ConversationsFragment.kt"
line="147" line="148"
column="29"/> column="29"/>
</issue> </issue>
@ -2566,7 +2566,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/conversation/ConversationsFragment.kt" file="src/main/java/app/pachli/components/conversation/ConversationsFragment.kt"
line="149" line="150"
column="37"/> column="37"/>
</issue> </issue>
@ -2654,7 +2654,7 @@
errorLine2=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/java/app/pachli/network/InstanceSwitchAuthInterceptor.kt" file="src/main/java/app/pachli/network/InstanceSwitchAuthInterceptor.kt"
line="42" line="43"
column="29"/> column="29"/>
</issue> </issue>
@ -2665,7 +2665,7 @@
errorLine2=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/java/app/pachli/network/InstanceSwitchAuthInterceptor.kt" file="src/main/java/app/pachli/network/InstanceSwitchAuthInterceptor.kt"
line="51" line="52"
column="37"/> column="37"/>
</issue> </issue>
@ -2929,7 +2929,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/login/LoginActivity.kt" file="src/main/java/app/pachli/components/login/LoginActivity.kt"
line="154" line="155"
column="22"/> column="22"/>
</issue> </issue>
@ -2940,7 +2940,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt" file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
line="142" line="143"
column="17"/> column="17"/>
</issue> </issue>
@ -2951,7 +2951,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt" file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
line="142" line="143"
column="17"/> column="17"/>
</issue> </issue>
@ -2962,7 +2962,7 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt" file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
line="151" line="152"
column="17"/> column="17"/>
</issue> </issue>
@ -2973,7 +2973,7 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt" file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
line="151" line="152"
column="17"/> column="17"/>
</issue> </issue>
@ -2984,7 +2984,7 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt" file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
line="172" line="173"
column="25"/> column="25"/>
</issue> </issue>
@ -2995,7 +2995,7 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt" file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
line="172" line="173"
column="25"/> column="25"/>
</issue> </issue>
@ -3006,7 +3006,7 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt" file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
line="175" line="176"
column="25"/> column="25"/>
</issue> </issue>
@ -3017,7 +3017,7 @@
errorLine2=" ~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt" file="src/main/java/app/pachli/components/login/LoginWebViewActivity.kt"
line="175" line="176"
column="25"/> column="25"/>
</issue> </issue>
@ -3457,7 +3457,7 @@
errorLine2=" ~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/compose/MediaPreviewAdapter.kt" file="src/main/java/app/pachli/components/compose/MediaPreviewAdapter.kt"
line="138" line="139"
column="17"/> column="17"/>
</issue> </issue>
@ -3688,7 +3688,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/fragment/SFragment.kt" file="src/main/java/app/pachli/fragment/SFragment.kt"
line="182" line="183"
column="48"/> column="48"/>
</issue> </issue>
@ -3732,7 +3732,7 @@
errorLine2=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/report/adapter/StatusViewHolder.kt" file="src/main/java/app/pachli/components/report/adapter/StatusViewHolder.kt"
line="59" line="60"
column="13"/> column="13"/>
</issue> </issue>
@ -3743,7 +3743,7 @@
errorLine2=" ~~~~~~~~"> errorLine2=" ~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/report/adapter/StatusViewHolder.kt" file="src/main/java/app/pachli/components/report/adapter/StatusViewHolder.kt"
line="65" line="66"
column="13"/> column="13"/>
</issue> </issue>
@ -3798,7 +3798,7 @@
errorLine2=" ~~~~~~~~~"> errorLine2=" ~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/components/timeline/TimelineFragment.kt" file="src/main/java/app/pachli/components/timeline/TimelineFragment.kt"
line="174" line="193"
column="47"/> column="47"/>
</issue> </issue>
@ -3809,7 +3809,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/fragment/ViewImageFragment.kt" file="src/main/java/app/pachli/fragment/ViewImageFragment.kt"
line="217" line="218"
column="25"/> column="25"/>
</issue> </issue>
@ -3820,7 +3820,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/fragment/ViewImageFragment.kt" file="src/main/java/app/pachli/fragment/ViewImageFragment.kt"
line="305" line="306"
column="34"/> column="34"/>
</issue> </issue>
@ -3831,7 +3831,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/fragment/ViewImageFragment.kt" file="src/main/java/app/pachli/fragment/ViewImageFragment.kt"
line="318" line="319"
column="13"/> column="13"/>
</issue> </issue>
@ -3842,7 +3842,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/fragment/ViewImageFragment.kt" file="src/main/java/app/pachli/fragment/ViewImageFragment.kt"
line="325" line="326"
column="17"/> column="17"/>
</issue> </issue>
@ -3908,7 +3908,7 @@
errorLine2=" ~~~~~~~"> errorLine2=" ~~~~~~~">
<location <location
file="src/main/java/app/pachli/fragment/ViewVideoFragment.kt" file="src/main/java/app/pachli/fragment/ViewVideoFragment.kt"
line="152" line="153"
column="32"/> column="32"/>
</issue> </issue>
@ -3919,7 +3919,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/fragment/ViewVideoFragment.kt" file="src/main/java/app/pachli/fragment/ViewVideoFragment.kt"
line="232" line="233"
column="21"/> column="21"/>
</issue> </issue>
@ -5213,7 +5213,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/java/app/pachli/util/ShareShortcutHelper.kt" file="src/main/java/app/pachli/util/ShareShortcutHelper.kt"
line="86" line="87"
column="9"/> column="9"/>
</issue> </issue>
@ -5224,7 +5224,7 @@
errorLine2=" ^"> errorLine2=" ^">
<location <location
file="src/main/java/app/pachli/adapter/TabAdapter.kt" file="src/main/java/app/pachli/adapter/TabAdapter.kt"
line="91" line="92"
column="13"/> column="13"/>
</issue> </issue>
@ -5235,7 +5235,7 @@
errorLine2=" ^"> errorLine2=" ^">
<location <location
file="src/main/java/app/pachli/adapter/TabAdapter.kt" file="src/main/java/app/pachli/adapter/TabAdapter.kt"
line="91" line="92"
column="50"/> column="50"/>
</issue> </issue>

View File

@ -4,6 +4,7 @@ import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.pachli.appstore.EventHub import app.pachli.appstore.EventHub
import app.pachli.appstore.FilterChangedEvent
import app.pachli.entity.Filter import app.pachli.entity.Filter
import app.pachli.entity.FilterKeyword import app.pachli.entity.FilterKeyword
import app.pachli.network.MastodonApi import app.pachli.network.MastodonApi
@ -88,9 +89,21 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
val action = action.value.action val action = action.value.action
return withContext(viewModelScope.coroutineContext) { return withContext(viewModelScope.coroutineContext) {
originalFilter?.let { filter -> val success = originalFilter?.let { filter ->
updateFilter(filter, title, contexts, action, durationIndex, context) updateFilter(filter, title, contexts, action, durationIndex, context)
} ?: createFilter(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
} }
} }

View File

@ -75,6 +75,9 @@ class FiltersViewModel @Inject constructor(
api.deleteFilterV1(filter.id).fold( api.deleteFilterV1(filter.id).fold(
{ {
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED) 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() Snackbar.make(parent, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()

View File

@ -307,7 +307,6 @@ class NotificationsViewModel @Inject constructor(
private val timelineCases: TimelineCases, private val timelineCases: TimelineCases,
private val eventHub: EventHub, private val eventHub: EventHub,
private val filtersRepository: FiltersRepository, private val filtersRepository: FiltersRepository,
private val filterModel: FilterModel,
statusDisplayOptionsRepository: StatusDisplayOptionsRepository, statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
private val sharedPreferencesRepository: SharedPreferencesRepository, private val sharedPreferencesRepository: SharedPreferencesRepository,
) : ViewModel() { ) : ViewModel() {
@ -349,9 +348,9 @@ class NotificationsViewModel @Inject constructor(
viewModelScope.launch { uiAction.emit(action) } viewModelScope.launch { uiAction.emit(action) }
} }
init { private var filterModel: FilterModel? = null
filterModel.kind = Filter.Kind.NOTIFICATIONS
init {
// Handle changes to notification filters // Handle changes to notification filters
val notificationFilter = uiAction val notificationFilter = uiAction
.filterIsInstance<InfallibleUiAction.ApplyFilter>() .filterIsInstance<InfallibleUiAction.ApplyFilter>()
@ -472,8 +471,11 @@ class NotificationsViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
eventHub.events eventHub.events
.filterIsInstance<FilterChangedEvent>() .filterIsInstance<FilterChangedEvent>()
.distinctUntilChanged() .filter { it.filterKind == Filter.Kind.NOTIFICATIONS }
.map { getFilters() } .map {
getFilters()
repository.invalidate()
}
.onStart { getFilters() } .onStart { getFilters() }
.collect() .collect()
} }
@ -516,7 +518,7 @@ class NotificationsViewModel @Inject constructor(
return repository.getNotificationsStream(filter = filters, initialKey = initialKey) return repository.getNotificationsStream(filter = filters, initialKey = initialKey)
.map { pagingData -> .map { pagingData ->
pagingData.map { notification -> 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( NotificationViewData.from(
notification, notification,
isShowingContent = statusDisplayOptions.value.showSensitiveMedia || isShowingContent = statusDisplayOptions.value.showSensitiveMedia ||
@ -531,25 +533,12 @@ class NotificationsViewModel @Inject constructor(
} }
} }
/** /** Gets the current filters from the repository. */
* 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.
*/
private fun getFilters() = viewModelScope.launch { private fun getFilters() = viewModelScope.launch {
try { try {
when (val filters = filtersRepository.getFilters()) { filterModel = when (val filters = filtersRepository.getFilters()) {
is FilterKind.V1 -> { is FilterKind.V1 -> FilterModel(Filter.Kind.NOTIFICATIONS, filters.filters)
filterModel.initWithFilters( is FilterKind.V2 -> FilterModel(Filter.Kind.NOTIFICATIONS)
filters.filters.filter {
it.context.contains("notifications")
},
)
repository.invalidate()
}
is FilterKind.V2 -> repository.invalidate()
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
_uiErrorChannel.send(UiError.GetFilters(throwable)) _uiErrorChannel.send(UiError.GetFilters(throwable))

View File

@ -29,9 +29,11 @@ import android.view.accessibility.AccessibilityManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.DEFAULT_ARGS_KEY
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -99,11 +101,30 @@ class TimelineFragment :
RefreshableFragment, RefreshableFragment,
MenuProvider { 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 { private val viewModel: TimelineViewModel by lazy {
if (timelineKind == TimelineKind.Home) { if (timelineKind == TimelineKind.Home) {
viewModels<CachedTimelineViewModel>().value viewModels<CachedTimelineViewModel>(
extrasProducer = {
MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(DEFAULT_ARGS_KEY, TimelineViewModel.creationExtras(timelineKind))
}
},
).value
} else { } 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)!! timelineKind = arguments.getParcelable(KIND_ARG)!!
viewModel.init(timelineKind)
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
adapter = TimelinePagingAdapter(this, viewModel.statusDisplayOptions.value) adapter = TimelinePagingAdapter(this, viewModel.statusDisplayOptions.value)

View File

@ -18,6 +18,7 @@
package app.pachli.components.timeline.viewmodel package app.pachli.components.timeline.viewmodel
import android.util.Log import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
@ -30,11 +31,9 @@ import app.pachli.appstore.PinEvent
import app.pachli.appstore.ReblogEvent import app.pachli.appstore.ReblogEvent
import app.pachli.components.timeline.CachedTimelineRepository import app.pachli.components.timeline.CachedTimelineRepository
import app.pachli.components.timeline.FiltersRepository import app.pachli.components.timeline.FiltersRepository
import app.pachli.components.timeline.TimelineKind
import app.pachli.db.AccountManager import app.pachli.db.AccountManager
import app.pachli.entity.Filter import app.pachli.entity.Filter
import app.pachli.entity.Poll import app.pachli.entity.Poll
import app.pachli.network.FilterModel
import app.pachli.usecase.TimelineCases import app.pachli.usecase.TimelineCases
import app.pachli.util.SharedPreferencesRepository import app.pachli.util.SharedPreferencesRepository
import app.pachli.util.StatusDisplayOptionsRepository import app.pachli.util.StatusDisplayOptionsRepository
@ -51,8 +50,10 @@ import javax.inject.Inject
/** /**
* TimelineViewModel that caches all statuses in a local database * TimelineViewModel that caches all statuses in a local database
*/ */
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel @HiltViewModel
class CachedTimelineViewModel @Inject constructor( class CachedTimelineViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val repository: CachedTimelineRepository, private val repository: CachedTimelineRepository,
timelineCases: TimelineCases, timelineCases: TimelineCases,
eventHub: EventHub, eventHub: EventHub,
@ -60,39 +61,33 @@ class CachedTimelineViewModel @Inject constructor(
accountManager: AccountManager, accountManager: AccountManager,
statusDisplayOptionsRepository: StatusDisplayOptionsRepository, statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
sharedPreferencesRepository: SharedPreferencesRepository, sharedPreferencesRepository: SharedPreferencesRepository,
filterModel: FilterModel,
private val gson: Gson, private val gson: Gson,
) : TimelineViewModel( ) : TimelineViewModel(
savedStateHandle,
timelineCases, timelineCases,
eventHub, eventHub,
filtersRepository, filtersRepository,
accountManager, accountManager,
filterModel,
statusDisplayOptionsRepository, statusDisplayOptionsRepository,
sharedPreferencesRepository, sharedPreferencesRepository,
) { ) {
override lateinit var statuses: Flow<PagingData<StatusViewData>> override var statuses: Flow<PagingData<StatusViewData>>
init { init {
readingPositionId = activeAccount.lastVisibleHomeTimelineStatusId readingPositionId = activeAccount.lastVisibleHomeTimelineStatusId
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun init(timelineKind: TimelineKind) {
super.init(timelineKind)
statuses = reload.flatMapLatest { statuses = reload.flatMapLatest {
getStatuses(timelineKind, initialKey = getInitialKey()) getStatuses(initialKey = getInitialKey())
}.cachedIn(viewModelScope) }.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( private fun getStatuses(
kind: TimelineKind,
initialKey: String? = null, initialKey: String? = null,
): Flow<PagingData<StatusViewData>> { ): Flow<PagingData<StatusViewData>> {
Log.d(TAG, "getStatuses: kind: $kind, initialKey: $initialKey") Log.d(TAG, "getStatuses: kind: $timelineKind, initialKey: $initialKey")
return repository.getStatusStream(kind = kind, initialKey = initialKey) return repository.getStatusStream(kind = timelineKind, initialKey = initialKey)
.map { pagingData -> .map { pagingData ->
pagingData pagingData
.map { .map {

View File

@ -18,6 +18,7 @@
package app.pachli.components.timeline.viewmodel package app.pachli.components.timeline.viewmodel
import android.util.Log import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
@ -30,11 +31,9 @@ import app.pachli.appstore.PinEvent
import app.pachli.appstore.ReblogEvent import app.pachli.appstore.ReblogEvent
import app.pachli.components.timeline.FiltersRepository import app.pachli.components.timeline.FiltersRepository
import app.pachli.components.timeline.NetworkTimelineRepository import app.pachli.components.timeline.NetworkTimelineRepository
import app.pachli.components.timeline.TimelineKind
import app.pachli.db.AccountManager import app.pachli.db.AccountManager
import app.pachli.entity.Filter import app.pachli.entity.Filter
import app.pachli.entity.Poll import app.pachli.entity.Poll
import app.pachli.network.FilterModel
import app.pachli.usecase.TimelineCases import app.pachli.usecase.TimelineCases
import app.pachli.util.SharedPreferencesRepository import app.pachli.util.SharedPreferencesRepository
import app.pachli.util.StatusDisplayOptionsRepository import app.pachli.util.StatusDisplayOptionsRepository
@ -50,8 +49,10 @@ import javax.inject.Inject
/** /**
* TimelineViewModel that caches all statuses in an in-memory list * TimelineViewModel that caches all statuses in an in-memory list
*/ */
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel @HiltViewModel
class NetworkTimelineViewModel @Inject constructor( class NetworkTimelineViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val repository: NetworkTimelineRepository, private val repository: NetworkTimelineRepository,
timelineCases: TimelineCases, timelineCases: TimelineCases,
eventHub: EventHub, eventHub: EventHub,
@ -59,36 +60,32 @@ class NetworkTimelineViewModel @Inject constructor(
accountManager: AccountManager, accountManager: AccountManager,
statusDisplayOptionsRepository: StatusDisplayOptionsRepository, statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
sharedPreferencesRepository: SharedPreferencesRepository, sharedPreferencesRepository: SharedPreferencesRepository,
filterModel: FilterModel,
) : TimelineViewModel( ) : TimelineViewModel(
savedStateHandle,
timelineCases, timelineCases,
eventHub, eventHub,
filtersRepository, filtersRepository,
accountManager, accountManager,
filterModel,
statusDisplayOptionsRepository, statusDisplayOptionsRepository,
sharedPreferencesRepository, sharedPreferencesRepository,
) { ) {
private val modifiedViewData = mutableMapOf<String, StatusViewData>() private val modifiedViewData = mutableMapOf<String, StatusViewData>()
override lateinit var statuses: Flow<PagingData<StatusViewData>> override var statuses: Flow<PagingData<StatusViewData>>
@OptIn(ExperimentalCoroutinesApi::class) init {
override fun init(timelineKind: TimelineKind) {
super.init(timelineKind)
statuses = reload statuses = reload
.flatMapLatest { .flatMapLatest {
getStatuses(timelineKind, initialKey = getInitialKey()) getStatuses(initialKey = getInitialKey())
}.cachedIn(viewModelScope) }.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( private fun getStatuses(
kind: TimelineKind,
initialKey: String? = null, initialKey: String? = null,
): Flow<PagingData<StatusViewData>> { ): Flow<PagingData<StatusViewData>> {
Log.d(TAG, "getStatuses: kind: $kind, initialKey: $initialKey") Log.d(TAG, "getStatuses: kind: $timelineKind, initialKey: $initialKey")
return repository.getStatusStream(viewModelScope, kind = kind, initialKey = initialKey) return repository.getStatusStream(viewModelScope, kind = timelineKind, initialKey = initialKey)
.map { pagingData -> .map { pagingData ->
pagingData.map { pagingData.map {
modifiedViewData[it.id] ?: StatusViewData.from( modifiedViewData[it.id] ?: StatusViewData.from(

View File

@ -20,6 +20,9 @@ package app.pachli.components.timeline.viewmodel
import android.util.Log import android.util.Log
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.core.os.bundleOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
@ -253,11 +256,11 @@ sealed interface UiError {
} }
abstract class TimelineViewModel( abstract class TimelineViewModel(
savedStateHandle: SavedStateHandle,
private val timelineCases: TimelineCases, private val timelineCases: TimelineCases,
private val eventHub: EventHub, private val eventHub: EventHub,
private val filtersRepository: FiltersRepository, private val filtersRepository: FiltersRepository,
protected val accountManager: AccountManager, protected val accountManager: AccountManager,
private val filterModel: FilterModel,
statusDisplayOptionsRepository: StatusDisplayOptionsRepository, statusDisplayOptionsRepository: StatusDisplayOptionsRepository,
private val sharedPreferencesRepository: SharedPreferencesRepository, private val sharedPreferencesRepository: SharedPreferencesRepository,
) : ViewModel() { ) : ViewModel() {
@ -296,8 +299,7 @@ abstract class TimelineViewModel(
viewModelScope.launch { uiAction.emit(action) } viewModelScope.launch { uiAction.emit(action) }
} }
var timelineKind: TimelineKind = TimelineKind.Home val timelineKind: TimelineKind = savedStateHandle.get<TimelineKind>(TIMELINE_KIND_TAG)!!
private set
private var filterRemoveReplies = false private var filterRemoveReplies = false
private var filterRemoveReblogs = false private var filterRemoveReblogs = false
@ -311,6 +313,8 @@ abstract class TimelineViewModel(
open var readingPositionId: String? = null open var readingPositionId: String? = null
protected set protected set
private var filterModel: FilterModel? = null
init { init {
viewModelScope.launch { viewModelScope.launch {
updateFiltersFromPreferences().collectLatest { updateFiltersFromPreferences().collectLatest {
@ -384,13 +388,6 @@ abstract class TimelineViewModel(
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = UiState(showFabWhileScrolling = true), initialValue = UiState(showFabWhileScrolling = true),
) )
}
@CallSuper
open fun init(timelineKind: TimelineKind) {
this.timelineKind = timelineKind
filterModel.kind = Filter.Kind.from(timelineKind)
if (timelineKind is TimelineKind.Home) { if (timelineKind is TimelineKind.Home) {
// Note the variable is "true if filter" but the underlying preference/settings text is "true if show" // 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 return Filter.Action.HIDE
} else { } else {
statusViewData.filterAction = filterModel.shouldFilterStatus(status.actionableStatus) statusViewData.filterAction = filterModel?.filterActionFor(status.actionableStatus) ?: Filter.Action.NONE
statusViewData.filterAction statusViewData.filterAction
} }
} }
/** Updates the current set of filters if filter-related preferences change */ /** 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 private fun updateFiltersFromPreferences() = eventHub.events
.filterIsInstance<FilterChangedEvent>() .filterIsInstance<FilterChangedEvent>()
.filter { filterContextMatchesKind(timelineKind, listOf(it.filterKind)) } .filter { filterContextMatchesKind(timelineKind, listOf(it.filterKind)) }
.distinctUntilChanged() .map {
.map { getFilters() } getFilters()
reloadKeepingReadingPosition()
}
.onStart { getFilters() } .onStart { getFilters() }
/** /** Gets the current filters from the repository. */
* 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.
*/
private fun getFilters() { private fun getFilters() {
viewModelScope.launch { viewModelScope.launch {
Log.d(TAG, "getFilters()") Log.d(TAG, "getFilters()")
try { try {
when (val filters = filtersRepository.getFilters()) { val filterKind = Filter.Kind.from(timelineKind)
is FilterKind.V1 -> { filterModel = when (val filters = filtersRepository.getFilters()) {
filterModel.initWithFilters( is FilterKind.V1 -> FilterModel(filterKind, filters.filters)
filters.filters.filter { is FilterKind.V2 -> FilterModel(filterKind)
filterContextMatchesString(timelineKind, it.context)
},
)
invalidate()
}
is FilterKind.V2 -> invalidate()
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Log.d(TAG, "updateFilter(): Error fetching filters: ${throwable.message}") Log.d(TAG, "updateFilter(): Error fetching filters: ${throwable.message}")
@ -619,12 +603,14 @@ abstract class TimelineViewModel(
private const val TAG = "TimelineViewModel" private const val TAG = "TimelineViewModel"
private val THROTTLE_TIMEOUT = 500.milliseconds private val THROTTLE_TIMEOUT = 500.milliseconds
fun filterContextMatchesString( /** Tag for the timelineKind in `savedStateHandle` */
timelineKind: TimelineKind, @VisibleForTesting(VisibleForTesting.PRIVATE)
filterContext: List<String>, const val TIMELINE_KIND_TAG = "timelineKind"
): Boolean {
return filterContext.contains(Filter.Kind.from(timelineKind).kind) /** Create extras for this view model */
} fun creationExtras(timelineKind: TimelineKind) = bundleOf(
TIMELINE_KIND_TAG to timelineKind,
)
fun filterContextMatchesKind( fun filterContextMatchesKind(
timelineKind: TimelineKind, timelineKind: TimelineKind,

View File

@ -23,6 +23,7 @@ import app.pachli.appstore.BlockEvent
import app.pachli.appstore.BookmarkEvent import app.pachli.appstore.BookmarkEvent
import app.pachli.appstore.EventHub import app.pachli.appstore.EventHub
import app.pachli.appstore.FavoriteEvent import app.pachli.appstore.FavoriteEvent
import app.pachli.appstore.FilterChangedEvent
import app.pachli.appstore.PinEvent import app.pachli.appstore.PinEvent
import app.pachli.appstore.ReblogEvent import app.pachli.appstore.ReblogEvent
import app.pachli.appstore.StatusComposedEvent import app.pachli.appstore.StatusComposedEvent
@ -36,7 +37,6 @@ import app.pachli.db.AccountEntity
import app.pachli.db.AccountManager import app.pachli.db.AccountManager
import app.pachli.db.TimelineDao import app.pachli.db.TimelineDao
import app.pachli.entity.Filter import app.pachli.entity.Filter
import app.pachli.entity.FilterV1
import app.pachli.entity.Status import app.pachli.entity.Status
import app.pachli.network.FilterModel import app.pachli.network.FilterModel
import app.pachli.network.MastodonApi import app.pachli.network.MastodonApi
@ -61,7 +61,6 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ViewThreadViewModel @Inject constructor( class ViewThreadViewModel @Inject constructor(
private val api: MastodonApi, private val api: MastodonApi,
private val filterModel: FilterModel,
private val timelineCases: TimelineCases, private val timelineCases: TimelineCases,
eventHub: EventHub, eventHub: EventHub,
accountManager: AccountManager, accountManager: AccountManager,
@ -89,6 +88,8 @@ class ViewThreadViewModel @Inject constructor(
val activeAccount: AccountEntity val activeAccount: AccountEntity
private var filterModel: FilterModel? = null
init { init {
activeAccount = accountManager.activeAccount!! activeAccount = accountManager.activeAccount!!
alwaysShowSensitiveMedia = activeAccount.alwaysShowSensitiveMedia alwaysShowSensitiveMedia = activeAccount.alwaysShowSensitiveMedia
@ -106,6 +107,11 @@ class ViewThreadViewModel @Inject constructor(
is StatusComposedEvent -> handleStatusComposedEvent(event) is StatusComposedEvent -> handleStatusComposedEvent(event)
is StatusDeletedEvent -> handleStatusDeletedEvent(event) is StatusDeletedEvent -> handleStatusDeletedEvent(event)
is StatusEditedEvent -> handleStatusEditedEvent(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() { private fun loadFilters() {
viewModelScope.launch { viewModelScope.launch {
try { try {
when (val filters = filtersRepository.getFilters()) { filterModel = when (val filters = filtersRepository.getFilters()) {
is FilterKind.V1 -> { is FilterKind.V1 -> FilterModel(Filter.Kind.THREAD, filters.filters)
filterModel.initWithFilters( is FilterKind.V2 -> FilterModel(Filter.Kind.THREAD)
filters.filters.filter { filter ->
filter.context.contains(FilterV1.THREAD)
},
)
}
is FilterKind.V2 -> filterModel.kind = Filter.Kind.THREAD
} }
updateStatuses() updateStatuses()
} catch (_: Exception) { } catch (_: Exception) {
@ -509,7 +508,7 @@ class ViewThreadViewModel @Inject constructor(
if (status.isDetailed) { if (status.isDetailed) {
true true
} else { } else {
status.filterAction = filterModel.shouldFilterStatus(status.status) status.filterAction = filterModel?.filterActionFor(status.status) ?: Filter.Action.NONE
status.filterAction != Filter.Action.HIDE status.filterAction != Filter.Action.HIDE
} }
} }

View File

@ -6,28 +6,28 @@ import app.pachli.entity.Status
import app.pachli.util.parseAsMastodonHtml import app.pachli.util.parseAsMastodonHtml
import java.util.Date import java.util.Date
import java.util.regex.Pattern 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. * Construct with [filterKind] that corresponds to the kind of timeline, and optionally the set
* 2. You call [shouldFilterStatus] to figure out what to display when you load statuses. * 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 pattern: Pattern? = null
private var v1 = false
lateinit var kind: Filter.Kind
fun initWithFilters(filters: List<FilterV1>) { init {
v1 = true pattern = v1filters?.let { list ->
this.pattern = makeFilter(filters) makeFilter(list.filter { it.context.contains(filterKind.kind) })
}
} }
fun shouldFilterStatus(status: Status): Filter.Action { /** @return the [Filter.Action] that should be applied to this status */
if (v1) { fun filterActionFor(status: Status): Filter.Action {
pattern?.let { pat ->
// Patterns are expensive and thread-safe, matchers are neither. // 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) { if (status.poll?.options?.any { matcher.reset(it.title).find() } == true) {
return Filter.Action.HIDE return Filter.Action.HIDE
@ -48,7 +48,7 @@ class FilterModel @Inject constructor() {
} }
val matchingKind = status.filtered?.filter { result -> val matchingKind = status.filtered?.filter { result ->
result.filter.kinds.contains(kind) result.filter.kinds.contains(filterKind)
} }
return if (matchingKind.isNullOrEmpty()) { return if (matchingKind.isNullOrEmpty()) {

View File

@ -41,7 +41,6 @@ class FilterV1Test {
@Before @Before
fun setup() { fun setup() {
filterModel = FilterModel()
val filters = listOf( val filters = listOf(
FilterV1( FilterV1(
id = "123", id = "123",
@ -101,14 +100,14 @@ class FilterV1Test {
), ),
) )
filterModel.initWithFilters(filters) filterModel = FilterModel(Filter.Kind.HOME, filters)
} }
@Test @Test
fun shouldNotFilter() { fun shouldNotFilter() {
assertEquals( assertEquals(
Filter.Action.NONE, Filter.Action.NONE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "should not be filtered"), mockStatus(content = "should not be filtered"),
), ),
) )
@ -118,7 +117,7 @@ class FilterV1Test {
fun shouldFilter_whenContentMatchesBadWord() { fun shouldFilter_whenContentMatchesBadWord() {
assertEquals( assertEquals(
Filter.Action.HIDE, Filter.Action.HIDE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "one two badWord three"), mockStatus(content = "one two badWord three"),
), ),
) )
@ -128,7 +127,7 @@ class FilterV1Test {
fun shouldFilter_whenContentMatchesBadWordPart() { fun shouldFilter_whenContentMatchesBadWordPart() {
assertEquals( assertEquals(
Filter.Action.HIDE, Filter.Action.HIDE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "one two badWordPart three"), mockStatus(content = "one two badWordPart three"),
), ),
) )
@ -138,7 +137,7 @@ class FilterV1Test {
fun shouldFilter_whenContentMatchesBadWholeWord() { fun shouldFilter_whenContentMatchesBadWholeWord() {
assertEquals( assertEquals(
Filter.Action.HIDE, Filter.Action.HIDE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "one two badWholeWord three"), mockStatus(content = "one two badWholeWord three"),
), ),
) )
@ -148,7 +147,7 @@ class FilterV1Test {
fun shouldNotFilter_whenContentDoesNotMatchWholeWord() { fun shouldNotFilter_whenContentDoesNotMatchWholeWord() {
assertEquals( assertEquals(
Filter.Action.NONE, Filter.Action.NONE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "one two badWholeWordTest three"), mockStatus(content = "one two badWholeWordTest three"),
), ),
) )
@ -158,7 +157,7 @@ class FilterV1Test {
fun shouldFilter_whenSpoilerTextDoesMatch() { fun shouldFilter_whenSpoilerTextDoesMatch() {
assertEquals( assertEquals(
Filter.Action.HIDE, Filter.Action.HIDE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus( mockStatus(
content = "should not be filtered", content = "should not be filtered",
spoilerText = "badWord should be filtered", spoilerText = "badWord should be filtered",
@ -171,7 +170,7 @@ class FilterV1Test {
fun shouldFilter_whenPollTextDoesMatch() { fun shouldFilter_whenPollTextDoesMatch() {
assertEquals( assertEquals(
Filter.Action.HIDE, Filter.Action.HIDE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus( mockStatus(
content = "should not be filtered", content = "should not be filtered",
spoilerText = "should not be filtered", spoilerText = "should not be filtered",
@ -185,7 +184,7 @@ class FilterV1Test {
fun shouldFilter_whenMediaDescriptionDoesMatch() { fun shouldFilter_whenMediaDescriptionDoesMatch() {
assertEquals( assertEquals(
Filter.Action.HIDE, Filter.Action.HIDE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus( mockStatus(
content = "should not be filtered", content = "should not be filtered",
spoilerText = "should not be filtered", spoilerText = "should not be filtered",
@ -199,7 +198,7 @@ class FilterV1Test {
fun shouldFilterPartialWord_whenWholeWordFilterContainsNonAlphanumericCharacters() { fun shouldFilterPartialWord_whenWholeWordFilterContainsNonAlphanumericCharacters() {
assertEquals( assertEquals(
Filter.Action.HIDE, Filter.Action.HIDE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "one two someone@twitter.com three"), mockStatus(content = "one two someone@twitter.com three"),
), ),
) )
@ -209,7 +208,7 @@ class FilterV1Test {
fun shouldFilterHashtags() { fun shouldFilterHashtags() {
assertEquals( assertEquals(
Filter.Action.HIDE, Filter.Action.HIDE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "#hashtag one two three"), mockStatus(content = "#hashtag one two three"),
), ),
) )
@ -219,7 +218,7 @@ class FilterV1Test {
fun shouldFilterHashtags_whenContentIsMarkedUp() { fun shouldFilterHashtags_whenContentIsMarkedUp() {
assertEquals( assertEquals(
Filter.Action.HIDE, 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>"), 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() { fun shouldNotFilterHtmlAttributes() {
assertEquals( assertEquals(
Filter.Action.NONE, Filter.Action.NONE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "<p><a href=\"https://foo.bar/\">https://foo.bar/</a> one two three</p>"), 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() { fun shouldNotFilter_whenFilterIsExpired() {
assertEquals( assertEquals(
Filter.Action.NONE, Filter.Action.NONE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "content matching expired filter should not be filtered"), mockStatus(content = "content matching expired filter should not be filtered"),
), ),
) )
@ -249,7 +248,7 @@ class FilterV1Test {
fun shouldFilter_whenFilterIsUnexpired() { fun shouldFilter_whenFilterIsUnexpired() {
assertEquals( assertEquals(
Filter.Action.HIDE, Filter.Action.HIDE,
filterModel.shouldFilterStatus( filterModel.filterActionFor(
mockStatus(content = "content matching unexpired filter should be filtered"), mockStatus(content = "content matching unexpired filter should be filtered"),
), ),
) )

View File

@ -19,12 +19,12 @@ package app.pachli.components.notifications
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import app.pachli.appstore.EventHub import app.pachli.appstore.EventHub
import app.pachli.components.timeline.FilterKind
import app.pachli.components.timeline.FiltersRepository import app.pachli.components.timeline.FiltersRepository
import app.pachli.components.timeline.MainCoroutineRule import app.pachli.components.timeline.MainCoroutineRule
import app.pachli.db.AccountEntity import app.pachli.db.AccountEntity
import app.pachli.db.AccountManager import app.pachli.db.AccountManager
import app.pachli.fakes.InMemorySharedPreferences import app.pachli.fakes.InMemorySharedPreferences
import app.pachli.network.FilterModel
import app.pachli.settings.AccountPreferenceDataStore import app.pachli.settings.AccountPreferenceDataStore
import app.pachli.usecase.TimelineCases import app.pachli.usecase.TimelineCases
import app.pachli.util.SharedPreferencesRepository import app.pachli.util.SharedPreferencesRepository
@ -51,7 +51,6 @@ abstract class NotificationsViewModelTestBase {
protected lateinit var viewModel: NotificationsViewModel protected lateinit var viewModel: NotificationsViewModel
private lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository private lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository
private lateinit var filtersRepository: FiltersRepository private lateinit var filtersRepository: FiltersRepository
private lateinit var filterModel: FilterModel
private val eventHub = EventHub() private val eventHub = EventHub()
@ -96,8 +95,9 @@ abstract class NotificationsViewModelTestBase {
) )
timelineCases = mock() timelineCases = mock()
filtersRepository = mock() filtersRepository = mock {
filterModel = mock() onBlocking { getFilters() } doReturn FilterKind.V2(emptyList())
}
sharedPreferencesRepository = SharedPreferencesRepository( sharedPreferencesRepository = SharedPreferencesRepository(
InMemorySharedPreferences(), InMemorySharedPreferences(),
@ -117,7 +117,6 @@ abstract class NotificationsViewModelTestBase {
timelineCases, timelineCases,
eventHub, eventHub,
filtersRepository, filtersRepository,
filterModel,
statusDisplayOptionsRepository, statusDisplayOptionsRepository,
sharedPreferencesRepository, sharedPreferencesRepository,
) )

View File

@ -17,6 +17,7 @@
package app.pachli.components.timeline package app.pachli.components.timeline
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import app.pachli.PachliApplication import app.pachli.PachliApplication
import app.pachli.appstore.EventHub 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.components.timeline.viewmodel.TimelineViewModel
import app.pachli.db.AccountManager import app.pachli.db.AccountManager
import app.pachli.entity.Account import app.pachli.entity.Account
import app.pachli.network.FilterModel
import app.pachli.network.MastodonApi import app.pachli.network.MastodonApi
import app.pachli.settings.AccountPreferenceDataStore import app.pachli.settings.AccountPreferenceDataStore
import app.pachli.usecase.TimelineCases import app.pachli.usecase.TimelineCases
@ -76,19 +76,19 @@ abstract class CachedTimelineViewModelTestBase {
@Inject @Inject
lateinit var sharedPreferencesRepository: SharedPreferencesRepository lateinit var sharedPreferencesRepository: SharedPreferencesRepository
private lateinit var cachedTimelineRepository: CachedTimelineRepository @Inject
lateinit var filtersRepository: FiltersRepository
@Inject
lateinit var cachedTimelineRepository: CachedTimelineRepository
private lateinit var accountPreferenceDataStore: AccountPreferenceDataStore private lateinit var accountPreferenceDataStore: AccountPreferenceDataStore
protected lateinit var timelineCases: TimelineCases protected lateinit var timelineCases: TimelineCases
private lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository private lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository
private lateinit var filtersRepository: FiltersRepository
private lateinit var filterModel: FilterModel
protected lateinit var viewModel: TimelineViewModel protected lateinit var viewModel: TimelineViewModel
private val eventHub = EventHub() 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 */ /** Empty error response, for API calls that return one */
private var emptyError: Response<ResponseBody> = Response.error(404, "".toResponseBody()) private var emptyError: Response<ResponseBody> = Response.error(404, "".toResponseBody())
@ -102,6 +102,7 @@ abstract class CachedTimelineViewModelTestBase {
reset(mastodonApi) reset(mastodonApi)
mastodonApi.stub { mastodonApi.stub {
onBlocking { getCustomEmojis() } doReturn NetworkResult.failure(Exception()) onBlocking { getCustomEmojis() } doReturn NetworkResult.failure(Exception())
onBlocking { getFilters() } doReturn NetworkResult.success(emptyList())
} }
accountManager.addAccount( accountManager.addAccount(
@ -123,16 +124,12 @@ abstract class CachedTimelineViewModelTestBase {
), ),
) )
cachedTimelineRepository = mock()
accountPreferenceDataStore = AccountPreferenceDataStore( accountPreferenceDataStore = AccountPreferenceDataStore(
accountManager, accountManager,
TestScope(), TestScope(),
) )
timelineCases = mock() timelineCases = mock()
filtersRepository = mock()
filterModel = mock()
statusDisplayOptionsRepository = StatusDisplayOptionsRepository( statusDisplayOptionsRepository = StatusDisplayOptionsRepository(
sharedPreferencesRepository, sharedPreferencesRepository,
@ -142,6 +139,7 @@ abstract class CachedTimelineViewModelTestBase {
) )
viewModel = CachedTimelineViewModel( viewModel = CachedTimelineViewModel(
SavedStateHandle(mapOf(TimelineViewModel.TIMELINE_KIND_TAG to TimelineKind.Home)),
cachedTimelineRepository, cachedTimelineRepository,
timelineCases, timelineCases,
eventHub, eventHub,
@ -149,11 +147,7 @@ abstract class CachedTimelineViewModelTestBase {
accountManager, accountManager,
statusDisplayOptionsRepository, statusDisplayOptionsRepository,
sharedPreferencesRepository, sharedPreferencesRepository,
filterModel,
Gson(), Gson(),
) )
// Initialisation with the Home timeline
viewModel.init(TimelineKind.Home)
} }
} }

View File

@ -17,17 +17,19 @@
package app.pachli.components.timeline package app.pachli.components.timeline
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import app.pachli.appstore.EventHub import app.pachli.appstore.EventHub
import app.pachli.components.timeline.viewmodel.NetworkTimelineViewModel import app.pachli.components.timeline.viewmodel.NetworkTimelineViewModel
import app.pachli.components.timeline.viewmodel.TimelineViewModel import app.pachli.components.timeline.viewmodel.TimelineViewModel
import app.pachli.db.AccountManager import app.pachli.db.AccountManager
import app.pachli.entity.Account import app.pachli.entity.Account
import app.pachli.network.FilterModel import app.pachli.network.MastodonApi
import app.pachli.settings.AccountPreferenceDataStore import app.pachli.settings.AccountPreferenceDataStore
import app.pachli.usecase.TimelineCases import app.pachli.usecase.TimelineCases
import app.pachli.util.SharedPreferencesRepository import app.pachli.util.SharedPreferencesRepository
import app.pachli.util.StatusDisplayOptionsRepository import app.pachli.util.StatusDisplayOptionsRepository
import at.connyduck.calladapter.networkresult.NetworkResult
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -36,7 +38,10 @@ import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.reset
import org.mockito.kotlin.stub
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
@ -57,22 +62,25 @@ abstract class NetworkTimelineViewModelTestBase {
@Inject @Inject
lateinit var accountManager: AccountManager lateinit var accountManager: AccountManager
@Inject
lateinit var mastodonApi: MastodonApi
@Inject @Inject
lateinit var sharedPreferencesRepository: SharedPreferencesRepository lateinit var sharedPreferencesRepository: SharedPreferencesRepository
private lateinit var networkTimelineRepository: NetworkTimelineRepository @Inject
lateinit var filtersRepository: FiltersRepository
@Inject
lateinit var networkTimelineRepository: NetworkTimelineRepository
private lateinit var accountPreferenceDataStore: AccountPreferenceDataStore private lateinit var accountPreferenceDataStore: AccountPreferenceDataStore
protected lateinit var timelineCases: TimelineCases protected lateinit var timelineCases: TimelineCases
private lateinit var filtersRepository: FiltersRepository
private lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository private lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository
private lateinit var filterModel: FilterModel
protected lateinit var viewModel: TimelineViewModel protected lateinit var viewModel: TimelineViewModel
private val eventHub = EventHub() 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 */ /** Empty error response, for API calls that return one */
private var emptyError: Response<ResponseBody> = Response.error(404, "".toResponseBody()) private var emptyError: Response<ResponseBody> = Response.error(404, "".toResponseBody())
@ -83,6 +91,12 @@ abstract class NetworkTimelineViewModelTestBase {
fun setup() { fun setup() {
hilt.inject() hilt.inject()
reset(mastodonApi)
mastodonApi.stub {
onBlocking { getCustomEmojis() } doReturn NetworkResult.failure(Exception())
onBlocking { getFilters() } doReturn NetworkResult.success(emptyList())
}
accountManager.addAccount( accountManager.addAccount(
accessToken = "token", accessToken = "token",
domain = "domain.example", domain = "domain.example",
@ -102,16 +116,12 @@ abstract class NetworkTimelineViewModelTestBase {
), ),
) )
networkTimelineRepository = mock()
accountPreferenceDataStore = AccountPreferenceDataStore( accountPreferenceDataStore = AccountPreferenceDataStore(
accountManager, accountManager,
TestScope(), TestScope(),
) )
timelineCases = mock() timelineCases = mock()
filtersRepository = mock()
filterModel = mock()
statusDisplayOptionsRepository = StatusDisplayOptionsRepository( statusDisplayOptionsRepository = StatusDisplayOptionsRepository(
sharedPreferencesRepository, sharedPreferencesRepository,
@ -121,6 +131,7 @@ abstract class NetworkTimelineViewModelTestBase {
) )
viewModel = NetworkTimelineViewModel( viewModel = NetworkTimelineViewModel(
SavedStateHandle(mapOf(TimelineViewModel.TIMELINE_KIND_TAG to TimelineKind.Bookmarks)),
networkTimelineRepository, networkTimelineRepository,
timelineCases, timelineCases,
eventHub, eventHub,
@ -128,11 +139,6 @@ abstract class NetworkTimelineViewModelTestBase {
accountManager, accountManager,
statusDisplayOptionsRepository, statusDisplayOptionsRepository,
sharedPreferencesRepository, sharedPreferencesRepository,
filterModel,
) )
// Initialisation with any timeline kind, as long as it's not Home
// (Home uses CachedTimelineViewModel)
viewModel.init(TimelineKind.Bookmarks)
} }
} }

View File

@ -18,7 +18,6 @@ import app.pachli.db.AccountManager
import app.pachli.db.TimelineDao import app.pachli.db.TimelineDao
import app.pachli.entity.Account import app.pachli.entity.Account
import app.pachli.entity.StatusContext import app.pachli.entity.StatusContext
import app.pachli.network.FilterModel
import app.pachli.network.MastodonApi import app.pachli.network.MastodonApi
import app.pachli.settings.AccountPreferenceDataStore import app.pachli.settings.AccountPreferenceDataStore
import app.pachli.usecase.TimelineCases import app.pachli.usecase.TimelineCases
@ -130,8 +129,6 @@ class ViewThreadViewModelTest {
onBlocking { getFilters() } doReturn FilterKind.V2(emptyList()) onBlocking { getFilters() } doReturn FilterKind.V2(emptyList())
} }
val filterModel = FilterModel()
val defaultAccount = AccountEntity( val defaultAccount = AccountEntity(
id = 1, id = 1,
domain = "mastodon.test", domain = "mastodon.test",
@ -178,7 +175,6 @@ class ViewThreadViewModelTest {
viewModel = ViewThreadViewModel( viewModel = ViewThreadViewModel(
mastodonApi, mastodonApi,
filterModel,
timelineCases, timelineCases,
eventHub, eventHub,
accountManager, accountManager,