Besides the refactoring these improvements:
* Track last push distributor and reset settings and subscription on any
incompatible change (ie. uninstall)
* Only update (push) notification settings on server if needed
* Allow to only fetch notifications for one account (the one for which a
push message was received)
This is (also) the revival of
https://github.com/tuskyapp/Tusky/pull/3642
It's not really well tested so far. (Ie. with two or more accounts or
two or more push providers.)
- Move all database queries off the ui thread - this is a massive
performance improvement
- ViewModel for MainActivity - this makes MainActivity smaller and
network requests won't be retried when rotating the screen
- removes the Push Notification Migration feature. We had it long
enough, all users who want push notifications should be migrated by now
- AccountEntity is now immutable
- converted BaseActivity to Kotlin
- The header image of Accounts is now cached as well
Do summary notifications like the Api defines it:
* Schedule and summarize without delay (in order for summerization to
work)
* Always have a summary notification: simplify code with this and make
more reliable
* Do not care about single notification count (the system already does
that as well)
* **Bugfix: Schedule summary first: This avoids a rate limit problem
that (then) not groups at all**
Testing this is probably the most difficult part.
For example I couldn't get any notification to ring with older Api
versions in the debugger. (Same as for current develop)
However one hack to always get notifications: Fix "minId" in
"fetchNewNotifications()" to a somewhat older value.
Next possible step: Have only one summary notification at all (for all
channels/notification types). You can still configure single channels
differently.
Or: For very many notifications: Only use a true summary one (something
like "you have 28 favorites and 7 boosts").
Generally: The notification timeline must be improved now. Because that
must be the go-to solution for any large number of notifications. It
must be easy to read. E. g. with grouping per post.
This was so much work wow. I think it works pretty well and is the best
compromise between all the alternative we considered. Yes the
pull-to-refreh on the notifications works slightly different now when
the new bar is visible, but I don't think there is a way around that.
Things I plan to do later, i.e. not as part of this PR or release:
- Cache the notification policy summary for better offline behavior and
less view shifting when it loads
- try to reduce some of the code duplications that are now in there
- if there is user demand, add a "legacy mode" setting where this
feature is disabled even if the server would support it
closes#4331closes#4550 as won't do
closes#4712 as won't do
<img
src="https://github.com/user-attachments/assets/de322d3c-3775-41e7-be57-28ab7fbaecdf"
width="240"/> <img
src="https://github.com/user-attachments/assets/1ce958a4-4f15-484c-a337-5ad93f36046c"
width="240"/> <img
src="https://github.com/user-attachments/assets/98b0482b-1c05-4c99-a371-f7f4d8a69abd"
width="240"/>
- use `runTest` instead of `runBlocking`, where possible
- run all Robolectric tests on Api 34 (where we have most users)
- some new testcase for `TimestampUtilsTest`
- move our only instrumented Android Test, `MigrationsTest`, to unit
test so it runs in CI and expand it to test all migrations
- upgrade Robolectric
- removed truth and espresso as they are no longer needed
```
java.lang.StringIndexOutOfBoundsException
at android.text.SpannableStringBuilder.<init>(SpannableStringBuilder.java:63)
at android.text.SpannableStringBuilder.subSequence(SpannableStringBuilder.java:1198)
at com.keylesspalace.tusky.util.LinkHelper.setClickableText(LinkHelper.kt:99)
at com.keylesspalace.tusky.adapter.StatusBaseViewHolder.setTextVisible(StatusBaseViewHolder.java:289)
at com.keylesspalace.tusky.adapter.StatusBaseViewHolder.setSpoilerAndContent(StatusBaseViewHolder.java:244)
at com.keylesspalace.tusky.adapter.StatusBaseViewHolder.setupWithStatus(StatusBaseViewHolder.java:820)
at com.keylesspalace.tusky.adapter.StatusViewHolder.setupWithStatus(StatusViewHolder.java:91)
at com.keylesspalace.tusky.components.timeline.TimelinePagingAdapter.bindViewHolder(TimelinePagingAdapter.kt:100)
at com.keylesspalace.tusky.components.timeline.TimelinePagingAdapter.onBindViewHolder(TimelinePagingAdapter.kt:82)
at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7847)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:6646)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6917)
at androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:288)
at androidx.recyclerview.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:345)
at androidx.recyclerview.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:361)
at androidx.recyclerview.widget.GapWorker.prefetch(GapWorker.java:368)
at androidx.recyclerview.widget.GapWorker.run(GapWorker.java:399)
at android.os.Handler.handleCallback(Handler.java:959)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loopOnce(Looper.java:232)
at android.os.Looper.loop(Looper.java:317)
at android.app.ActivityThread.main(ActivityThread.java:8705)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
```
Before we would not send `expires_in` when "indefinite" was selected.
But that left the expiration at the value it was before. To actually set
it to indefinite we need to send `expires_in`, but leave it empty.
With a value class this was actually really nice to fix, the code now
self-documents what the special values mean.
Also fixes a regression from the Material 3 redesign where the filter
duration drop down would not get populated when creating a filter.
Found while working on https://github.com/tuskyapp/Tusky/pull/4742
```
android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY)
at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:961)
at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:790)
at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:89)
at androidx.sqlite.db.framework.FrameworkSQLiteStatement.executeInsert(FrameworkSQLiteStatement.kt:42)
at androidx.room.EntityInsertionAdapter.insertAndReturnId(EntityInsertionAdapter.kt:101)
at com.keylesspalace.tusky.db.dao.TimelineStatusDao_Impl$insert$2.call(TimelineStatusDao_Impl.kt:345)
at com.keylesspalace.tusky.db.dao.TimelineStatusDao_Impl$insert$2.call(TimelineStatusDao_Impl.kt:340)
at androidx.room.CoroutinesRoom$Companion.execute(CoroutinesRoom.kt:56)
at com.keylesspalace.tusky.db.dao.TimelineStatusDao_Impl.insert(TimelineStatusDao_Impl.kt:340)
at com.keylesspalace.tusky.components.notifications.NotificationsRemoteMediator.replaceNotificationRange(NotificationsRemoteMediator.kt:169)
at com.keylesspalace.tusky.components.notifications.NotificationsRemoteMediator.access$replaceNotificationRange(NotificationsRemoteMediator.kt:36)
at com.keylesspalace.tusky.components.notifications.NotificationsRemoteMediator$load$3.invokeSuspend(NotificationsRemoteMediator.kt:109)
at com.keylesspalace.tusky.components.notifications.NotificationsRemoteMediator$load$3.invoke(Unknown Source:8)
at com.keylesspalace.tusky.components.notifications.NotificationsRemoteMediator$load$3.invoke(Unknown Source:2)
at androidx.room.RoomDatabaseKt$withTransaction$transactionBlock$1.invokeSuspend(RoomDatabaseExt.kt:62)
at androidx.room.RoomDatabaseKt$withTransaction$transactionBlock$1.invoke(Unknown Source:8)
at androidx.room.RoomDatabaseKt$withTransaction$transactionBlock$1.invoke(Unknown Source:4)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:61)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:163)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
at androidx.room.RoomDatabaseKt$startTransactionCoroutine$2$1$1.invokeSuspend(RoomDatabaseExt.kt:103)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at androidx.room.RoomDatabaseKt$startTransactionCoroutine$2$1.run(RoomDatabaseExt.kt:99)
at androidx.room.TransactionExecutor.execute$lambda$1$lambda$0(TransactionExecutor.kt:36)
at androidx.room.TransactionExecutor.$r8$lambda$FZWr2PGmP3sgXLCiri-DCcePXSs(Unknown Source:0)
at androidx.room.TransactionExecutor$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)
```
It looks kinda weird because "x just posted" has a different user than
the actual post, but it works for groups I guess? And definitely better
than crashing.
<img
src="https://github.com/user-attachments/assets/8110ff17-674d-4f36-8df0-453a666856a6"
width="320"/>
closes#4563
closes#4631closes#4629
and other weirdness introduced in Tusky 26.1.
I did a lot of testing on 2 physical devices and multiple emulators. It
definitely is better than before, but probably still not perfect.
This does 2 things:
- Removes `AccountSwitchInterceptor`, the main culprit for the bug. APIs
can no longer change their base url after they have been created. As a
result they are not Singletons anymore.
- Additionally, I refactored how MainActivity handles Intents to make it
less likely to have multiple instances of it active.
Here is how I could reliably reproduce the bug:
- Be logged in with account A and B
- Write a post with account A, cancel it before it sends (go into flight
mode for that)
- Switch to account B
- Open the "this post failed to send" notification from account A,
drafts will open
- Go back. You are in the MainActivity of account A, everything seems
fine.
- Go back again. You are in the old, now broken MainActivity of account
B. It uses the database of account B but the network of account A.
Refreshing will show posts from A.
closes#4567closes#4554closes#4402closes#4148closes#2663
and possibly #4588
Instead of calling the endpoint every time filters are needed, it will
be called only once and the result cached. This will result in quite
some requests less on instances supporting v2.
I also tested v1 filters and made some small improvements. We should
[remove filters v1
support](https://github.com/tuskyapp/Tusky/issues/4538) some time in the
future though.
This PR fixes https://github.com/tuskyapp/Tusky/issues/2798 and is
mostly based on and supersedes
https://github.com/tuskyapp/Tusky/pull/2826 but I have fixed all merge
conflicts and unit tests.
I tested the changes locally and the setting takes effect immediately
for replies, and persists across killing the app.
---------
Co-authored-by: Eva Tatarka <eva@tatarka.me>
Co-authored-by: Konrad Pozniak <connyduck@users.noreply.github.com>
At first I thought simply changing the regex might help, but then I
found more and more differences between Mastodon and Tusky, so I decided
to reimplement the thing. I added 74 testcases that I all compared to
Mastodon to make sure they are correct.
On an Fairphone 4 the new implementation is faster, on an Samsung Galaxy
Tab S3 slower.
Testcases for the benchmark:
```
test of a status with #one hashtag http
```
```
test
http:// #hashtag https://connyduck.at/http://example.org
this is a #test
and this is a @mention@test.com @test @test@test456@test.com
```
```
@mention@test.social Just your ordinary mention with a hashtag
#test
```
```
@mention@test.social Just your ordinary mention with a url
https://riot.im/app/#/room/#Tusky:matrix.org
```
FP4:
```
11.159 ns 15 allocs Benchmark.new_1
119.701 ns 43 allocs Benchmark.new_2
21.895 ns 24 allocs Benchmark.new_3
87.512 ns 32 allocs Benchmark.new_4
16.592 ns 46 allocs Benchmark.old_1
134.381 ns 169 allocs Benchmark.old_2
28.355 ns 68 allocs Benchmark.old_3
45.221 ns 77 allocs Benchmark.old_4
```
SGT3:
```
43,785 ns 18 allocs Benchmark.new_1
446,074 ns 43 allocs Benchmark.new_2
78,802 ns 26 allocs Benchmark.new_3
315,478 ns 32 allocs Benchmark.new_4
42,186 ns 45 allocs Benchmark.old_1
353,570 ns 157 allocs Benchmark.old_2
72,376 ns 66 allocs Benchmark.old_3
122,985 ns 74 allocs Benchmark.old_4
```
benchmark code is here: https://github.com/tuskyapp/tusky-span-benchmark
closes https://github.com/tuskyapp/Tusky/issues/4425
(this one is for @charlag)
Calling `PreferenceManager.getDefaultSharedPreferences()` will read the
preference file from disk every time. This PR makes `SharedPreferences`
a singleton so they will only be created once at appstart (with a few
exceptions where it is hard to inject, e.g. in the `openLink` helper)
which should help getting our ANRs down.
```
StrictMode policy violation; ~duration=285 ms: android.os.strictmode.DiskReadViolation
at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1666)
at libcore.io.BlockGuardOs.access(BlockGuardOs.java:74)
at libcore.io.ForwardingOs.access(ForwardingOs.java:128)
at android.app.ActivityThread$AndroidOs.access(ActivityThread.java:8054)
at java.io.UnixFileSystem.checkAccess(UnixFileSystem.java:313)
at java.io.File.exists(File.java:813)
at android.app.ContextImpl.ensurePrivateDirExists(ContextImpl.java:790)
at android.app.ContextImpl.ensurePrivateDirExists(ContextImpl.java:781)
at android.app.ContextImpl.getPreferencesDir(ContextImpl.java:737)
at android.app.ContextImpl.getSharedPreferencesPath(ContextImpl.java:962)
at android.app.ContextImpl.getSharedPreferences(ContextImpl.java:583)
at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:221)
at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:221)
at androidx.preference.PreferenceManager.getDefaultSharedPreferences(PreferenceManager.java:119)
at com.keylesspalace.tusky.BaseActivity.onCreate(BaseActivity.java:96)
...
```
Hilt is an annotation processor built on top of Dagger which allows to
remove all the Android dependency injection boilerplate code (currently
around 900 lines) by writing it for us.
Hilt can use KSP instead of Kapt so Kapt can be completely removed from
the project. Kapt is slow, deprecated and has a few compatibility
issues. Removing Kapt will improve build times since no Java stubs have
to be generated for Kotlin classes anymore (Note that KSP also processes
annotations in Java classes so it can completely replace Kapt).
- Remove all modules related to manual dependency injection
configuration.
- Rename `AppModule` to `StorageModule` since it now only contains
configuration to retrieve the DataBase and SharedPreferences.
- Annotate all entry points (Activities, Fragments, BroadcastReceivers
and Services) with `@AndroidEntryPoint`.
- Annotate all injected ViewModels with `@HiltViewModel` and replace the
custom ViewModel Factory with the default one (which integrates with the
one generated by Hilt).
- Add a public field to allow overriding the default
ViewModelProvider.Factory in `BaseActivity` in tests.
- Annotate tested Activities with `@OptionalInject` since Activity tests
currently rely on the Activities not being injected automatically.
- Annotate injected `Context` arguments with `@ApplicationContext`. Hilt
provides the `Context` binding automatically but requires to specify if
the Application or Activity Context is wanted.
- Add WorkManager Hilt integration so all Workers are injected by Hilt
automatically using `HiltWorkerFactory`.
- Lazily initialize WorkManager in `TuskyApplication`.
- Remove Kapt and Kapt workarounds.
- ~~Remove toolchain configuration for Java 21. Toolchains force the
Java bytecode to match the JDK version used to build the project, and
apparently Hilt doesn't run inside the toolchain so cannot process the
source code if the JDK version of the toolchain is higher than the JDK
used to run Gradle. [And configuring a toolchain for an older Java
version causes other
issues](https://jakewharton.com/gradle-toolchains-are-rarely-a-good-idea/).
**Removing toolchains configuration doesn't prevent the project from
being built using JDK 21** or more recent versions but allows to build
the project using older JDKs as well.~~
Added a fix to allow Hilt to properly use the JDK toolchain.
- ~~Set the Java and Kotlin bytecode target to Java 17. The standard
bytecode target for Android projects is usually Java 8 or 11 (any higher
version doesn't provide any benefit but may cause compatibility issues).
However, since the app currently uses a library built against Java 17
bytecode (`networkresult-calladapter`), it needs to target at least Java
17 bytecode as well.~~
- Update the Dagger 2 URL in the licenses screen. Hilt is part of Dagger
2 so the label wasn't changed.
This refactors the NotificationsFragment and related classes to Kotlin &
paging.
While trying to preserve as much of the original behavior as possible,
this adds the following improvements as well:
- The "show notifications filter" preference was added again
- The "load more" button now has a background ripple effect when clicked
- The "legal" report category of Mastodon 4.2 is now supported in report
notifications
- Unknown notifications now display "unknown notification type" instead
of an empty line
Other code quality improvements:
- All views from xml layouts are now referenced via ViewBindings
- the classes responsible for showing system notifications were moved to
a new package `systemnotifications` while the classes from this
refactoring are in `notifications`
- the id of the local Tusky account is now called `tuskyAccountId` in
all places I could find
closes https://github.com/tuskyapp/Tusky/issues/3429
---------
Co-authored-by: Zongle Wang <wangzongler@gmail.com>
**! ! Warning**: Do not merge before testing every API call and database
read involving JSON !
**Gson** is obsolete and has been superseded by **Moshi**. But more
importantly, parsing Kotlin objects using Gson is _dangerous_ because
Gson uses Java serialization and is **not Kotlin-aware**. This has two
main consequences:
- Fields of non-null types may end up null at runtime. Parsing will
succeed, but the code may crash later with a `NullPointerException` when
trying to access a field member;
- Default values of constructor parameters are always ignored. When
absent, reference types will be null, booleans will be false and
integers will be zero.
On the other hand, Kotlin-aware parsers like **Moshi** or **Kotlin
Serialization** will validate at parsing time that all received fields
comply with the Kotlin contract and avoid errors at runtime, making apps
more stable and schema mismatches easier to detect (as long as logs are
accessible):
- Receiving a null value for a non-null type will generate a parsing
error;
- Optional types are declared explicitly by adding a default value. **A
missing value with no default value declaration will generate a parsing
error.**
Migrating the entity declarations from Gson to Moshi will make the code
more robust but is not an easy task because of the semantic differences.
With Gson, both nullable and optional fields are represented with a null
value. After converting to Moshi, some nullable entities can become
non-null with a default value (if they are optional and not nullable),
others can stay nullable with no default value (if they are mandatory
and nullable), and others can become **nullable with a default value of
null** (if they are optional _or_ nullable _or_ both). That third option
is the safest bet when it's not clear if a field is optional or not,
except for lists which can usually be declared as non-null with a
default value of an empty list (I have yet to see a nullable array type
in the Mastodon API).
Fields that are currently declared as non-null present another
challenge. In theory, they should remain as-is and everything will work
fine. In practice, **because Gson is not aware of nullable types at
all**, it's possible that some non-null fields currently hold a null
value in some cases but the app does not report any error because the
field is not accessed by Kotlin code in that scenario. After migrating
to Moshi however, parsing such a field will now fail early if a null
value or no value is received.
These fields will have to be identified by heavily testing the app and
looking for parsing errors (`JsonDataException`) and/or by going through
the Mastodon documentation. A default value needs to be added for
missing optional fields, and their type could optionally be changed to
nullable, depending on the case.
Gson is also currently used to serialize and deserialize objects to and
from the local database, which is also challenging because backwards
compatibility needs to be preserved. Fortunately, by default Gson omits
writing null fields, so a field of type `List<T>?` could be replaced
with a field of type `List<T>` with a default value of `emptyList()` and
reading back the old data should still work. However, nullable lists
that are written directly (not as a field of another object) will still
be serialized to JSON as `"null"` so the deserializing code must still
be handling null properly.
Finally, changing the database schema is out of scope for this pull
request, so database entities that also happen to be serialized with
Gson will keep their original types even if they could be made non-null
as an improvement.
In the end this is all for the best, because the app will be more
reliable and errors will be easier to detect by showing up earlier with
a clear error message. Not to mention the performance benefits of using
Moshi compared to Gson.
- Replace Gson reflection with Moshi Kotlin codegen to generate all
parsers at compile time.
- Replace custom `Rfc3339DateJsonAdapter` with the one provided by
moshi-adapters.
- Replace custom `JsonDeserializer` classes for Enum types with
`EnumJsonAdapter.create(T).withUnknownFallback()` from moshi-adapters to
support fallback values.
- Replace `GuardedBooleanAdapter` with the more generic `GuardedAdapter`
which works with any type. Any nullable field may now be annotated with
`@Guarded`.
- Remove Proguard rules related to Json entities. Each Json entity needs
to be annotated with `@JsonClass` with no exception, and adding this
annotation will ensure that R8/Proguard will handle the entities
properly.
- Replace some nullable Boolean fields with non-null Boolean fields with
a default value where possible.
- Replace some nullable list fields with non-null list fields with a
default value of `emptyList()` where possible.
- Update `TimelineDao` to perform all Json conversions internally using
`Converters` so no Gson or Moshi instance has to be passed to its
methods.
- ~~Create a custom `DraftAttachmentJsonAdapter` to serialize and
deserialize `DraftAttachment` which is a special entity that supports
more than one json name per field. A custom adapter is necessary because
there is not direct equivalent of `@SerializedName(alternate = [...])`
in Moshi.~~ Remove alternate names for some `DraftAttachment` fields
which were used as a workaround to deserialize local data in 2-years old
builds of Tusky.
- Update tests to make them work with Moshi.
- Simplify a few `equals()` implementations.
- Change a few functions to `val`s
- Turn `NetworkModule` into an `object` (since it contains no abstract
methods).
Please test the app thoroughly before merging. There may be some fields
currently declared as mandatory that are actually optional.
This pull request removes the remaining RxJava code and replaces it with
coroutine-equivalent implementations.
- Remove all duplicate methods in `MastodonApi`:
- Methods returning a RxJava `Single` have been replaced by suspending
methods returning a `NetworkResult` in order to be consistent with the
new code.
- _sync_/_async_ method variants are replaced with the _async_ version
only (suspending method), and `runBlocking{}` is used to make the async
variant synchronous.
- Create a custom coroutine-based implementation of `Single` for usage
in Java code where launching a coroutine is not possible. This class can
be deleted after remaining Java code has been converted to Kotlin.
- `NotificationsFragment.java` can subscribe to `EventHub` events by
calling the new lifecycle-aware `EventHub.subscribe()` method. This
allows using the `SharedFlow` as single source of truth for all events.
- Rx Autodispose is replaced by `lifecycleScope.launch()` which will
automatically cancel the coroutine when the Fragment view/Activity is
destroyed.
- Background work is launched in the existing injectable
`externalScope`, since using `GlobalScope` is discouraged.
`externalScope` has been changed to be a `@Singleton` and to use the
main dispatcher by default.
- Transform `ShareShortcutHelper` to an injectable utility class so it
can use the application `Context` and `externalScope` as provided
dependencies to launch a background coroutine.
- Implement a custom Glide extension method
`RequestBuilder.submitAsync()` to do the same thing as
`RequestBuilder.submit().get()` in a non-blocking way. This way there is
no need to switch to a background dispatcher and block a background
thread, and cancellation is supported out-of-the-box.
- An utility method `Fragment.updateRelativeTimePeriodically()` has been
added to remove duplicate logic in `TimelineFragment` and
`NotificationsFragment`, and the logic is now implemented using a simple
coroutine instead of `Observable.interval()`. Note that the periodic
update now happens between onStart and onStop instead of between
onResume and onPause, since the Fragment is not interactive but is still
visible in the started state.
- Rewrite `BottomSheetActivityTest` using coroutines tests.
- Remove all RxJava library dependencies.
builds upon work from #4082
Additionally fixes some deprecations and adds support for [predictive
back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture).
I also refactored how the activity transitions work because they are
closely related to predictive back. The awkward
`finishWithoutSlideOutAnimation` is gone, activities that have been
started with slide in will now automatically close with slide out.
To test predictive back you need an emulator or device with Sdk 34
(Android 14) and then enable it in the developer settings.
Predictive back requires the back action to be determined before it
actually occurs so the system can play the right predictive animation,
which made a few reorganisations necessary.
closes#4082closes#4005
unlocks a bunch of dependency upgrades that require sdk 34
---------
Co-authored-by: Goooler <wangzongler@gmail.com>
There are some new rules, I think they mostly make sense, except for the
max line length which I had to disable because we are over it in a lot
of places.
---------
Co-authored-by: Goooler <wangzongler@gmail.com>
The idea here is: Everytime we get hold of a new version of a post, we
update everything about that post everywhere.
This makes the distincion between different event types unnecessary, as
everythng is just a `StatusChangedEvent`.
The main benefit is that posts should be up-to-date more often, which is
important considering there is now editing and #3413
While helping test an issue with
[Bookwyrm](https://github.com/bookwyrm-social/bookwyrm) I noticed that
the URL formats used by that project aren't checked as possible profile
or post links. They're quite close to a couple of others, so I just
copied close examples and edited a couple of terms.
It's pretty minor, I just used a previous commit as a reference. Let me
know if it needs anything more though. I've only quickly tested it on a
local build with a couple of links against a live Bookwyrm and it picks
them up as expected now.
Previously the notification filter and clear actions were shown as
buttons in the UI, with a preference that determined whether they were
displayed.
Remove this preference, and display them as menu items.
- "Filter notifications" is shown as an icon, if possible
- "Clear notifications" is only ever shown as a menu item, to reduce the
chance the user inadvertently selects it
To ensure that the options menu appears correctly, remove the code that
creates a "fake" action bar, and adjust the layouts so that there are
three toolbars;
- mainToolbar -- displays the icons, and the current "location" (Home,
Notifications, etc)
- topNav -- displays the row of tabs at the top
- bottomNav -- displays the row of tabs at the bottom
Only one of them is set as the support action bar (depending on the
user's preferences). This provides the "show a logo" and "show the
options menu" functionality as standard, without needing to re-implement
as the previous code did.
Update to Kotlin 1.9.0 and migrate to newer language idioms.
- Remove unnecessary @OptIn for features migrated to mainstream
- Use `data object` where appropriate
- Use new enum `entries` property
As tests are run against locale JVM and test does not force
a locale to run, so some tests may fail due to a different result only
due to the locale of the JVM used.
Example here with test `same year formatting` in class
`AbsoluteTimeFormatterTest` line 30 on a French JVM. There may be other
lines to fail with other languages.
Fixes#3859
Introduce Flow<T>.throttleFirst(). In a flow this emits the first value,
and each value afterwards that is > some timeout after the previous
value.
This prevents accidental double-taps on UI elements from generating
multiple-actions.
The previous code used debounce(). That has a similar effect, but with
debounce() the code has to wait until after the timeout period has
elapsed before it can process the action, leading to an unnecessary
UI delay.
With throttleFirst a value is emitted immediately, there's no need
to wait. It's subsequent values that are potentially throttled.
formatNumber() was existing code to show numbers with suffixes like K, M, etc, so re-use that code and delete shortNumber().
Update the tests to (a) test formatNumber(), and (b) be parameterised.