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=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue