Release builds normally strip out all logging to reduce the number of
disk writes and reduce UI jank.
These logs would still be useful in user error reports from orange
builds. To preseve them:
- Implement a simple `RingBuffer`.
- Create `TreeRing`, a `Timber` `Tree` logger that logs to a
`RingBuffer` instance in orange release builds.
- Create `TreeRingCollector`, called when ACRA reports are generated,
which includes the contents of the ring buffer in the report.
- Enable desugaring to allow the use of java.time libraries on older
Android versions.
- Disable ProGuard obfuscation of class names as the obfuscation adds
additional de-obfuscation steps when handling error reports from users.
Getting error reports with logs of strange behaviour is useful even if
the app doesn't crash.
Move crash reporting in to `core.activity`, and provide a menu option
(in orange builds) to trigger a non-fatal crash report that is handled
the same way (i.e., sent by e-mail) as a regular crash report.
`BaseActivity` has to be able to create and handle menus, so adjust
subclasses to call the superclass when necessary.
Update `tools/mvstring` to be able to move strings between different
flavour directories, not just `main`.
Continue modularisation by moving activities in the "About" feature to a
new `feature.about` module.
Implement `feature.about:
- Move `AboutActivity`, `LicenseActivity`, and `PrivacyPolicyActivity`
here.
- Update `markdown2resource` plugin to work with libraries
Implement `core.data`:
- Types and repositories used through the app
- Move `InstanceInfo` and `InstanceInfoRepository` here so they are
available to `feature.about`.
Implement `core.ui`:
- App-specific views, spans, and other UI content
- Move `ClickableSpanTextView` and `NoUnderlineURLSpan` here so they are
available to `feature.about`.
Continue modularisation by moving core activity classes that almost all
activities depende on to a `core.activity` module. This includes
core "helper" classes as well.
Implement core.activity:
- Contains BaseActivity, BottomSheetActivity
- Contains LinkHelper and other utility classes used by activities
Implement core.common.extensions:
- Move ViewBindingExtensions and ViewExtensions here
Implement core.common.util:
- Move BlurHashDecoder and VersionName here
Implement core.designsystem:
- Holds common resources (animations, colours, drawables, etc) used
through the app
- Import "core.designsystem.R as DR" through the app to distinguish
from the module's own resources
Implement feature.login:
- Move the LoginActivity and related code/resources to its own module
Implement tools/mvstring
- Moves string resources (and all translations) from one module to
another
Previous code would show errors when fetching server info but with no
mechanism to retry the operation, so it would "stick" until the user
restarted the app.
Fix this with a retry action.
While I'm here use styles to ensure that all snackbars are 5 lines max
length and remove code that sets the length.
Previously when the user interacted with a status the operation (reblog,
favourite, etc) travels through multiple layers of code, carrying with
it the position of the item in the list that the user operated on.
At some point the status is retrieved from the list using its position
so that the correct status ID can be used in the network operation.
If this happens while the list is also refreshing there's a possible
race condition, and the original status' position may have changed in
the list. Looking up the status by position to determine which status to
perform the action on may cause the action to happen on the wrong
status.
Fix this by passing the status' viewdata to any actions instead of its
position. This includes all the information necessary to make the API
call, so there is no chance of a race.
This is quite an involved change because there are three types of
viewdata:
- `StatusViewData`, used for regular timelines
- `NotificationViewData`, used for notifications, may wrap a status that
can be operated on
- `ConversationViewData`, used for conversations, does wrap a status
The previous code treated them all differently, which is probably why it
operated by position instead of type.
The high level fix is to:
1. Create an interface, `IStatusViewData`, that contains the data
exposed by any viewdata that contains a status.
2. Implement the interface in `StatusViewData`, `NotificationViewData`,
and `ConversationViewData`.
3. Change the code that operates on viewdata (`SFragment`,
`StatusActionListener`, etc) to be generic over anything that implements
`IStatusViewData`.
4. Change the code that handles actions to pass the viewdata instead of
the position.
Fixes#370
Previous code created the drawer image loader as an anonymous inner
class that kept a reference to `glide`, which had a reference to `this`
and could leak.
Fix that by create a separate class that takes these as constructor
parameters.
The previous code incorrectly showed the trending tag usage data twice
next to the end of the trending tag lines, instead of one entry for the
usage data and one entry for the account data.
Fix that.
As part of this fix change how the data is displayed. Instead of using
two distinct `TextView`, fixed to the bottom end of the chart, draw the
text directly on the chart. The text is accurately position so that it
is next to the end of the relevant line. If both lines overlap the label
positions are adjusted appropriately.
The chart now uses Pachli blue and orange for the line colours.
While doing this I discovered that the mechanism used to fall back to
particular chart colours if none were specified was incorrect, so fix
that too.
Previous code could return an error on Friendica version strings like
`2024.03-dev-1547`.
Fix this:
- Extend the list of explicitly supported servers to include Fedibird,
Friendica, Glitch, Hometown, Iceshrimp, Pixelfed, and Sharkey.
- Add version parsing routines for these servers.
- Test the version parsing routines fetching every server and version
seen by Fediverse Observer (~ 2,000 servers) and ensuring that the
server and version information can be parsed.
Improve the error message:
- Show the hostname with a `ServerRepository` error
Clean up the code:
- Remove the custom `resultOf` and `mapResult` functions, they have
equivalents in newer versions of the library (like `runSuspendCatching`)
Fixes#372
The previous code unilaterally enabled filter functionality. Some
Mastodon-like servers -- like GoToSocial -- do not support filters, and
this resulted in user visible error messages when connecting to those
servers.
To fix this:
- Extend the set of supported server capabilities to include client and
server side filtering.
- Disable the filter preferences if the server does not support filters
and show a message explaining why it's disabled.
Extend the capabilities model to support this:
- Fetch server software name and version from the nodeinfo endpoints
(implementing the nodeinfo API and schema)
- Extend the use of kotlin-result to provide hierarchies of Error
classes and demonstrate how to chain errors and display more informative
messages without using exceptions.
Fixes#343
Previously some tests had to manually create dependencies (instead of
injecting them) because the dependency required the `TestScope`
`CoroutineScope` as one of its dependencies.
Resolve this with a `FakeCoroutineScopeModule` that provides `TestScope`
as `@ApplicationScope`. The tests can now inject their dependencies,
which will use `TestScope`.
To inject `AccountPreferenceDataStore` it has been updated to use the
current active account when reading or writing preferences.
e35fa1db inadvertently contained some left over debug code that treats
non-bot accounts as bots (for displaying the bot badge) and vice versa.
Fixes#321
If a status was part of a thread, and it was not the "detailed" status,
and it had been translated, then the view data was marked as "show the
translation". But the translation was not loaded, so the status content
appeared as empty.
Fix that by loading the translated content of all statuses in the thead
and ensure that the translated content is rendered.
Throw an `IllegalStateException` in debug builds to catch any future
occurrences of this.
Fixes#281
Add a dependency on ACRA (in orange builds only), and catch crashes.
The user is given the option to e-mail the crash report data to the
support address, and can view and edit/redact the data before doing so.
Mastodon counts post lengths by considering emojis to be single
characters, no matter how many unicode code points they are composed of.
So "😜" has length 1.
Pachli was using `String.length`, which considers "😜" as length 2.
Correct the calculation by using a BreakIterator to count the characters
in the string, which treats multi-character emojis as a length 1.
Poll options had a similar problem, exacerbated by the Mastodon web UI
also having the same problem, see
https://github.com/mastodon/mastodon/issues/28336.
Fix that by creating `MastodonLengthFilter`, an `InputFilter` that does
the right thing for regular text that may contain emojis.
See also https://github.com/tuskyapp/Tusky/pull/4152, which has the fix
for status length but not polls.
---------
Co-authored-by: Konrad Pozniak <opensource@connyduck.at>
Roles for the logged in user appeared in Mastodon 4.0.0 and can be
displayed on the user's profile screen.
Show them as chips, adjusting the display of the existing "Follows you"
and "Bot" indicators to make allowances for this.
Roles can have a custom colour assigned by the server admin. This is
blended with the app colour so it is not too jarring in the display.
See also https://github.com/tuskyapp/Tusky/pull/4029
Co-authored-by: Konrad Pozniak <opensource@connyduck.at>
Quoting @connyduck in https://github.com/tuskyapp/Tusky/pull/4150:
"""
The transformation ends up in Glide's memory cache and leaks whole
Activities through the view -> context reference.
This fixes the problem by removing the background detection logic (so
the view reference is no longer needed) and setting the background
directly instead. Looks exactly as before.
"""
Co-authored-by: Konrad Pozniak <opensource@connyduck.at>
The previous code generally started an activity by having the activity
provide a method in a companion object that returns the relevant intent,
possibly taking additional parameters that will be included in the
intent as extras.
E.g., if A wants to start B, B provides the method that returns the
intent that starts B.
This introduces a dependency between A and B.
This is worse if B also wants to start A.
For example, if A is `StatusListActivity` and B is`ViewThreadActivity`.
The user might click a status in `StatusListActivity` to view the
thread, starting `ViewThreadActivity`. But from the thread they might
click a hashtag to view the list of statuses with that hashtag. Now
`StatusListActivity` and `ViewThreadActivity` have a circular
dependency.
Even if that doesn't happen the dependency means that any changes to B
will trigger a rebuild of A, even if the changes to B are not relevant.
Break this dependency by adding a `:core:navigation` module with an
`app.pachli.core.navigation` package that contains `Intent` subclasses
that should be used instead. The `quadrant` plugin is used to generate
constants that can be used to launch activities by name instead of by
class, breaking the dependency chain.
The plugin uses the `Activity` names from the manifest, so when an
activity is moved in the future the constant will automatically update
to reflect the new package name.
If the activity's intent requires specific extras those are passed via
the constructor, with companion object methods to extract them from the
intent.
Using the intent classes from this package is enforced by a lint
`IntentDetector` which will warn if any intents are created using a
class literal.
See #291
The package wasn't renamed when it was moved, so was still
`app.pachli.components.timeline`, instead of the new location,
`app.pachli.core.network.model`.
The existing code base is a single monolithic module. This is relatively
simple to configure, but many of the tasks to compile the module and
produce the final app have to run in series.
This is unnecessarily slow.
This change starts to split the code in to multiple modules, which are:
- :core:account - AccountManager, to break a dependency cycle
- :core:common - low level types or utilities used in many other modules
- :core:database - database types, DAOs, and DI infrastructure
- :core:network - network types, API definitions, and DI infrastructure
- :core:preferences - shared preferences definitions and DI
infrastructure
- :core:testing - fakes and rules used across different modules
Benchmarking with gradle-profiler shows a ~ 17% reduction in incremental
build times after an ABI change. That will improve further as more code
is moved to modules.
The rough mechanics of the changes are:
- Create the modules, and move existing files in to them. This causes a
lot of churn in import arguments.
- Convert build.gradle files to build.gradle.kts
- Separate out the data required to display a tab (`TabViewData`) from
the data required to configure a tab (`TabData`) to avoid circular
dependencies.
- Abstract the repeated build logic shared between the modules in to
a set of plugins under `build-logic/`, to simplify configuration of
the application and library builds.
- Be explicit that some nullable types are non-null at time of use.
Nullable properties in types imported from modules generally can't be
smart cast to non-null. There's a detailed discussion of why this
restriction exists at
https://discuss.kotlinlang.org/t/what-is-the-reason-behind-smart-cast-being-impossible-to-perform-when-referenced-class-is-in-another-module/2201.
The changes highlight design problems with the current code, including:
- The main application code is too tightly coupled to the network types
- Too many values are declared unnecessarily nullable
- Dependency cycles between code that make modularisation difficult
Future changes will add more modules.
See #291.
Previous code always called `startActivityAndCollapse()` with a regular
intent, which triggers an `UnsupportedOperationException` at API 34.
Use the non-deprecated variant that uses pending intents when
appropriate.
While looking at this I noticed the icon for the tile was incorrect, so
replaced that with the notification icon.
Show up to two lines of the link's title and three lines of the link's
description in link preview cards. This provides additional useful
context to the user, especially when many links bury the important
information at the end of the title.
Upgrading to this version of Pachli may trigger an Android bug where
cached animation specifications are not cleared, resulting in incorrect
animations (e.g., when navigating between activities).
This is an Android bug triggered by the Android Material library,
https://github.com/material-components/material-components-android/issues/3644.
Show the user a dialog (once) when launching after an upgrade, so they
know to restart their device if necessary.
Display the "compose" FAB when viewing a hashtag list. Tapping the
button will open `ComposeActivity` prepopulated with the hashtag at the
end of the post with the cursor at the start.
Fixes#228
Viewing edited statuses could crash on API levels around 26 with a
ResourceNotFoundException. Using `?colorOutline` for the divider colour
instead of `?android:textColorPrimary` fixes this, and is also a better
colour to use.
`Theme.Pachli` was being overriden on v29+ devices which meant that poll
options were showing with too much padding. Fix that by using `AppTheme`
as the final theme, and basing that off `Theme.Pachli`.
Black themes were using dark grey for toolbar and tab backgrounds, so
fix that too for a seamless experience with the black theme.
Not all subclasses were calling `super.onViewCreated()` so collecting
the server capability wasn't happening consistently. Fix this, and add a
`@CallSuper` annotation to prevent the problem from recurring.
Without this the model classes are not retained, which causes a
`ClassCastException` when parsing the new models for the instance v1 and
instance v2 API calls.
Fixes#250
Implement some support for server-side status translation. Do this by:
- Implement support for the `api/v1/instance` endpoint to determine if
the remote server supports translation.
- Create new `ServerCapabilities` to allow the app to query the remote
capabilities in a server-agnostic way. Use this to query if the
remote server supports the Mastodon implementation of server-side
translation
- If translation is supported then show a translate/undo translate
option on the status "..." menu.
- Fetch translated content from the server if requested, and store it
locally as a new Room entity.
- Update displaying a status to check if the translated version
should be displayed; if it should then new code is used to show
translated content, content warning, poll options, and media
descriptions.
- Add a `TextView` to show an "in progress" message while translation
is happening, and to show the translation provider (generally
required by agreements with them).
Partially fixes#62
---------
Co-authored-by: sanao <naosak1006@gmail.com>
Now that the flavour includes the store name it's not sufficient to
check for "orange" as the flavour, as that no longer matches. Now it
must start with "orange" to trigger using the git commit count as the
version code.
Users can inadvertently get stuck on older versions of the app; e.g., by
installing from one F-Droid repository that stops hosting the app at
some later time.
Analytics from the Play Store also shows a long tail of users who are,
for some reason, on an older version.
On resuming `MainActivity`, and approximately once per day, check and
see if a newer version of Pachli is available, and prompt the user to
update by going to the relevant install location (Google Play, F-Droid,
or GitHub).
The dialog prompt allows them to ignore this specific version, or
disable all future update notifications. This is also exposed through
the preferences, so the user can adjust it there too.
A different update check method is used for each installation location.
- F-Droid: Use the F-Droid API to query for the newest released version
- GitHub: Use the GitHub API to query for the newest release, and check
the APK filename attached to that release
- Google Play: Use the Play in-app-updates library
(https://developer.android.com/guide/playcore/in-app-updates) to query
for the newest released version
These are kept in different build flavours (source sets), so that e.g.,
the build for the F-Droid store can only query the F-Droid API, the UI
strings are specific to F-Droid, etc. This also ensures that the update
service libraries are specific to that build and do not
"cross-contaminate".
Note that this *does not* update the app, it takes the user to either
the relevant store page (F-Droid, Play) or GitHub release page. The user
must still start the update from that page.
CI configuration is updated to build the different flavours.
The preference change listener was being optimised out by R8, causing
rapid garbage collection, breaking the `changes` flow in release builds.
Fix this by annotating the field with `@Keep` so it is retained.
Fixes#225
Previous code always set `navigationBarColor` and `statusBarColor` to
`transparent` irrespective of the API level.
This only works on API 29 and above; if you do it on API levels lower
than that the system navigation buttons (home, back, recents) are
typically shown on a very similar colour to the background, making them
very hard to see.
Fixes#221
The previous code used `androidx.appcompat.widget.Toolbar` in a several
places.
It's better to use `MaterialToolbar` as that plays better with other
Material components.
Update the usage throughout the project.
In addition, implement a lint check that will prevent any future use
from creeping back in.
Fixes#28
Use Timber instead of `android.util.Log`. Removes the need for `TAG`
statics in companion objects, slightly simplifying the code. Opens the
door for some production logging in the future.
Previously the middle-of-screen progress spinner and the spinner that
appears on a swipe-to-refresh could get out of sync.
Fix this by removing the middle-of-screen progress spinner from relevant
fragments, as the swipe-to-refresh spinner shows the user that an
operation is in progress, and also clues them in to the fact that a
swipe-to-refresh is possible (by using the common UX control).
Fixes#75
There's a well-hidden `updateLintBaseline` task that does what the
custom `newLintBaseline` task does. Prefer the `update...` task to
reduce the amount of custom machinery in this build.
Previously, ending a drag on an image (that didn't result in dismissing
the fragment) animates the image back in to position restoring the X
axis scale factor.
The Y axis scale factor was not restored, potentially breaking the
image's aspect ratio. Restore the Y axis scale factor to fix this
(`ViewVideoFragment` already handles this correctly).
Fixes#202
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.
Previous code called filters that affect threads "Conversations".
Correct this to "Threads", to distinguish from "Direct Messages" which
can also be known as "conversations" (in the API in particular).
Previous copyright notice mentioned that the license should have been
distributed with Tusky. Correct that to Pachli.
This does not change the copyright assignment, only the instructions
as to where to find the license.
A "self-boost" is someone boosting their own post. Some people are
particularly prolific at this, and it can clutter the timeline. Provide
a new preference that allows the user to show/hide these boosts from
their timeline.
Most parts of the UI will truncate the display of long names (display
and user), but it makes sense to show them untruncated on the account's
profile screen.
The previous code (a) used an emoji as the prefix character when showing
the destination of an obscured link, and (b) made the destination part
of the link anchor text.
Using an emoji was a problem because the user can use different emoji
sets and it can give strange results. Making the destination part of the
link text made it difficult to distinguish at a glance where one link
ends and another starts.
Fix the emoji problem by replacing the emoji with a drawable.
Fix the destination problem by changing the string resource so it only
includes the destination part, and inserting it at the end of the link
instead of replacing the whole link. This means the disclosed
destination is not clickable, does not look like part of the link, and
stands out more.