Previous code used a normal ProgressBar and a coroutine to delay
hiding/showing the bar for a snappier UI perception.
This is built-in functionality in LinearProgressIndicator, so switch to
that.
While I'm here, implement the "Select list" dialog's layout as a layout
resource.
If the user has tabs containing one or more lists, and any of those
lists are renamed or deleted then the change should be reflected in the
tabs.
To do that:
`MainActivity`:
- Re-create tabs whenever lists are loaded and there's a list in a tab
- Compare lists-in-tabs by the ID of the list when restoring the user's
tab, so that a list rename doesn't lose their position.
`NetworkListsRepository`:
- Update the user's tab preferences whenever lists are loaded, removing
tabs that contain lists that have been deleted, and updating the
list's title for lists that have been renamed.
Fixes#192
Previously to view a list the user either had to add it to a tab, or tap
through "Lists" in the navigation menu to their list of lists, and then
tap the list they want.
Fix that, and show all their lists in a dedicated section in the menu,
with a new "Manage lists" entry that's functionality identical to the
old "Lists" entry (i.e., it shows their lists and allows them to create,
delete, and edit list settings).
To do that:
- Implement a proper `ListsRepository` as the single source of truth for
list implementation throughout the app. Expose the current list of lists
as a flow, with methods to perform operations on the lists.
- Collect the `ListsRepository` flow in `MainActivity` and use that to
populate the menu and update it whenever the user's lists change.
- Rewrite the activities and fragments that manipulate lists to use
`ListRepository`.
- Always show error snackbars when list operations fail. In particular,
the HTTP code and error are always shown.
- Delete the custom `Either` implementation, it's no longer used.
- Add types for modelling API responses and errors, `ApiResponse` and
`ApiError` respectively. The response includes the headers as well as
the body, so it can replace the use of `NetworkResult` and `Response`.
The actual result of the operation is expected to be carried in a
`com.github.michaelbull.result.Result` type. Implement a Retrofit call
adapter for these types.
Unit tests for these borrow heavily from
https://github.com/connyduck/networkresult-calladapter
Additional user-visible changes:
- Add an accessible "Refresh" menu item to `ListsActivity`.
- Adding a list to a tab has a dialog with a "Manage lists" option.
Previously that would close the dialog when clicked, so the user had to
re-open it on returning from list management. Now the dialog stays open.
- The soft keyboard automatically opens when creating or editing a list.
Previous code showed a small icon for account media that the user has
hidden.
Now determine the correct size / aspect ratio for the media and use that
to compute the placeholder (either a blurhash, or the link colour for
consistency with the view on a timeline).
Fixes#513
Previous code showed the toolbar and caption when displaying the image,
and the user has to tap the screen to dismiss it.
New code is consistent with the video viewer; the toolbar and caption
is displayed for two seconds, and then auto-hides.
- Tapping after the hide displays the toolbar and caption, which will
not auto-hide and requires a second tap to dismiss.
- Tapping the caption before it's hidden cancels the auto-hide
Fixes#505
The account logout process could fail due to API exceptions; network
errors for example, or if the user had already revoked the app's token
for that account. This would prevent the rest of the logout process
(cleaning database, etc) from completing.
Fix this by ignoring network errors during the logout process, and
always cleaning up account content in the database.
Fix a related issue where a deleted account might be recreated in a
partial state if the account's visible position was saved after it was
deleted. The recreated account couldn't do anything as it had no tokens,
but is very confusing.
Previously the only way to access notifications was to dedicate a tab to
them. Now notifications are available from the left-side navigation menu
so they're always accessible.
Add them to the top of the list, and swap the order of bookmarks and
favourites, assuming that users are more likely to want to see their
bookmarks than their favourites.
Move "Edit profile" to the bottom with the other settings options,
assuming that editing their profile does not happen very often, so
should not be at the top of the list.
Previously a regular function that created and subscribed to an rxJava
callable, now it's a suspending function that enforces Dispatchers.IO as
the context, launched in its own coroutine.
Previously a regular function launched with an rxJava scheduler, now
it's a suspending function that enforces Dispatchers.IO as the context,
launched in its own coroutine.
Highlights:
- Implement fragment transitions for video to improve the UX, video
won't start playing until the transition completes
- Remove rxJava
- Move duplicate code in to base classes
Details:
`MediaActionsListener`:
- Move to `ViewMediaFragment` as it's used by both subclasses
- Remove need for separate `VideoActionsListener`
- Rename methods to better reflect their purpose and improve readability
`ViewMediaFragment`:
- Move duplicated code from `ViewImageFragment` and `ViewVideoFragment`
- Rewrite code that handles fragment transitions to use a
`CompleteableDeferred` instead of `BehaviorSubject` (removes rxJava).
- Rename methods and properties to better reflect their purpose and
improve readability
- Add extra comments
`ViewImageFragment`:
- Rewrite code that handles fragment transitions to use a
`CompleteableDeferred` instead of `BehaviorSubject` (removes rxJava).
`ViewVideoFragment`:
- Implement fragment transitions for video to improve the UX, video
won't start playing until the transition completes
- Manage toolbar visibility with a coroutine instead of a handler
- Add extra comments
`ViewMediaActivity`:
- Rename properties to better reflect their purpose and improve
readability
- Add extra comments
`ImagePagerAdapter`:
- Rename properties to better reflect their purpose and improve
readability
- Add extra comments
New lint rules highlighted a potential crash; the use of named match
groups (used here when extracting server versions) requires API >= 26 or
throws an exception.
Use the group numbers instead of names when extracting the value, but
keep the group names in the regular expressions for readability.
The previous code would share media by either:
a. If it was an image, downloading the image using Glide in to a bitmap,
then recompressing as a PNG, saving, and sharing the resulting file.
b. Otherwise, create a temporary file, enqueue a DownloadManager request
to download the media in to the file, and immediately start sharing,
hoping that the download had completed in time.
Both approaches have problems:
In the "image" case the image was being downloaded (or retrieved from
the Glide cache), decompressed to a bitmap, then recompressed as a PNG.
This uses more memory, and doesn't share the original contents of the
file. E.g., if the original file was a JPEG that's lost (and the PNG
might well be larger than the source image).
In the second case the DownloadManager download is not guaranteed to
have completed (or even started) before the user chooses the share
destination. The destination could receive a partial or even empty file.
Fix both of those cases by always fully downloading the file before
sending the share intent. This guarantees the file is available to
share, and in its original format. Since this uses the same OkHttpClient
as the rest of the app the content is highly likely to be in the OkHttp
cache, so there will no extra network traffic because of this.
The previous code used synchronous (i.e., non-suspending) functions to
call the /api/v2/search and /api/v2/accounts/search endpoints.
This is not necessary as the search was always performed in a separate
thread.
Remove, and replace their usage with the equivalent functions that
suspend.
Once desugaring is enabled it needs to be enabled for up/down the
dependency chain, so enable it in the shared configuration defined by
the build convention code.
Highlighted a failing test that wasn't being run, so fix that too.
A filter's context (previously referred to as its `kind`) controls where
the filter is applied.
This was implemented as an enum with a specific property to control how
it would serialise when @FormUrlEncoded, and with a @Json annotation for
Moshi.
In addition, the model objects kept the filter context in its string
form throughout Pachli, requiring periodic conversion to/from the enum
type, making the code more complicated.
Fix this, by:
1. Converting the incoming JSON value to the enum type immediately, so
the rest of the code uses the enum constants exclusively.
2. Implement a Retrofit converter that serialises the enum value when
@FormUrlEncoded to the same string used in JSON serialisation
Many servers that claim to be Mastodon-API compatible are not, as
evidenced by the content they include in the responses to
`/api/v1/instance` and `/api/v2/instance` requests.
Work around the worst of the breakage by providing defaults or marking
some fields as nullable (with a default null).
Bugs have been reported against the relevant projects.
The `highlighted` property on a role may be absent. If it is this breaks
account parsing, including accounts in an /api/v2/instance response.
This, in turn, breaks determining server capabilities, including whether
or not translation is supported.
Set the default to `true`, which matches observed Mastodon behaviour.
A user is reporting that a refresh is happening in the middle of loading
content, borne out by the existing logs.
Those logs don't say what has triggered the refresh attempt, so add
additional logging whenever a refresh can occur to record what triggered
it.
Previously media on the "Media" tab was displayed scaled and cropped to
a square aspect ratio, effectively forcing the user to tap every image
to see it.
Now, display the images scaled but not cropped, layed out with
`StaggeredGridLayoutManager`. This shows each image in full (still
scaled) providing a better experience when scrolling down.
Scrolling up can occasionally introduce gaps in the grid as images are
re-placed as viewholders are reused. When this happens images animate to
a better position when scrolling stops.
Previous code injected `ApplicationContext`, which is not themed, and
caused a crash if the "update" dialog was shown.
Fix by passing the necesssary context in explicitly when the check is
performed.
Previous code expected all incoming enums values to map directly to
Kotlin enum constants.
This is a problem for servers with additional features -- e.g.,
"reaction" as a notification type.
Fix this with a new Moshi adapter that will set the incoming value to a
given constant if it's not recognised.
Apply this to the enum constants in core.network to ensure they are
handled.
Clean up enum handling in Converters.kt, ComposeViewModel.kt, and
Status.kt by using the existing `.ordinal` property and some extension
functions for idiomatic code.
Fixes#461
Previous code showed a generic placeholder for audio media on the
account's "Media" tab.
Fix this so the preview image is shown (if it's available).
- Move the "is this attachment previewable?" code to `Attachment` so it
can be reused here.
- Restructure the logic in `AccountMediaGridAdapter` to use the new
`isPreviewable()` method when deciding whether to show a preview.
- Attachments have dedicated placeholder drawables, use those when the
preview is not available.
Add an additional preference entry that triggers an update when tapped.
It also displays the earliest time of the next automatic update check as
the preference summary.
Move the code that performs the update check (and the logic for whether
to perform the check) out of `MainActivity` and in to `UpdateCheck` so
it's available from `PreferencesFragment`.
If the user increases the font size the labels for post statistics
(number of replies, etc) can crash in to each other.
To give more space for the text:
- Shrink the label font size
- Move the labels slightly left / tighter to the icon
- Allow the "bookmark" icon to move next to the "more" icon. There's
still 48dp of space for them, and this gives a little more space to the
other icons that have labels
Friendica can return a null `voted_on` property, in violation of the API
spec.
Introduce a `BooleanIfNull` annotation that will convert the `null` to
`false` if encountered.
While I'm here update the other adapters as classes on their relevant
annotations instead of standalone classes to keep the code consistent.
Fixes#455
Some users report that Pachli is not retrieving/displaying notifications
in a timely fashion.
To assist in diagnosing these errors, provide an additional set of tabs
on the "About" screen that contain information about how Pachli is
fetching notifications, and if not, why not.
Allow the user to save notification related logs and other details to a
file that can be attached to an e-mail or bug report.
Recording data:
- Provide a `NotificationConfig` singleton with properties to record
different aspects of the notification configuration. Update these
properties as different notification actions occur.
- Store logs in a `LogEntryEntity` table. Log events of interest with a
new `Timber` `LogEntryTree` that is planted in all cases.
- Add `PruneLogEntryEntityWorker` to trim saved logs to the last 48
hours.
Display:
- Add a `NotificationFragment` to `AboutActivity`. It hosts two other
fragments in tabs to show details from `NotificationConfig` and the
relevant logs, as well as controls for interacting with them.
Bug fixes:
- Filter out notifications with a null tag when processing active
notifications, prevents an NPE crash
Other changes:
- Log more details when errors occur so the bug reports are more helpful
The previous code was using a bold version of the default font for
inserted text. So if the user had set a custom font it was being
ignored.
Fix this by creating a bold version of the user's typeface.
Fixes#435
The `x` and `y` properties in `Attachment.Focus` may be null (not
documented as such, but observed in the wild).
Provide a `DefaultIfNull` adapter that can be applied to these to
replace null values with a sensible default.
Content that doesn't start with a block element generates a parse error.
This isn't normally seen from Mastodon servers but is seen from content
from other servers (e.g,. Akkoma), which can generate:
```
<a href="..."> some text
```
instead of:
```
<p><a href="..."> some text</p>
```
Work around this by ensuring that content-to-be-diffed is wrapped in a
`div`.
The previous code didn't clear the task stack or recreate `MainActivity`
when the active user was changed.
So if the user was logged in with account A and used "Compose" from a
notification sent to account B, the active account was switched to B but
the UI chrome wasn't. After exiting `ComposeActivity` they would be
looking at the timeline for account B but with a toolbar that showed
account A.
Fix this by clearing the task stack and explicitly recreating
`MainActivity` when forwarding intents to `ComposeActivity`.
Shortcuts were being updated using `MainActivity.this` as the `Context`
and leaking in `ShortcutManagerCompat`. Use the application context to
fix this.
When the `Notification.Type` enum was moved to a separate module it
resulted in exceptions trying to deserialize the enum:
```
java.lang.RuntimeException: Parcelable encountered ClassNotFoundException reading a Serializable object (name = app.pachli.core.network.model.Notification$Type)
```
This happened in debug builds without Proguard, so not a minification or
renaming issue.
Fix this by providing `putEnum`/`getEnum` extension functions on
`Bundle` that store the enum's ordinal value and restore it from that.
UnifiedPush broadcasts are used to trigger fetching new notifications.
Previously this was a normal one-time work request. Mark it as expedited
to increase the likelihood it will run soon.
- Use format strings so any overhead of building the string is only
incurred if the message is actually logged
- Pass throwables as the first parameter so they are logged with the
stacktrace
Moshi is faster to decode JSON at runtime, is actively maintained, has a
smaller memory and method footprint, and a slightly smaller APK size.
Moshi also correctly creates default constructor arguments instead of
leaving them null, which was a source of `NullPointerExceptions` when
using Gson.
The conversion broadly consisted of:
- Adding `@JsonClass(generateAdapter = true)` to data classes that
marshall to/from JSON.
- Replacing `@SerializedName(value = ...)` with `@Json(name = ...)`.
- Replacing Gson instances with Moshi in Retrofit, Hilt, and tests.
- Using Moshi adapters to marshall to/from JSON instead of Gson `toJson`
/ `fromJson`.
- Deleting `Rfc3339DateJsonAdapter` and related code, and using the
equivalent adapter bundled with Moshi.
- Rewriting `GuardedBooleanAdapter` as a more generic `GuardedAdapter`.
- Deleting unused ProGuard rules; Moshi generates adapters using code
generation, not runtime reflection.
The conversion surfaced some bugs which have been fixed.
- Not all audio attachments have attachment size metadata. Don't show
the attachment preview if the metadata is missing.
- Some `throwable` were not being logged correctly.
- The wrong type was being used when parsing the response when sending a
scheduled status.
- Exceptions other than `HttpException` or `IoException` would also
cause a status to be resent. If there's a JSON error parsing a response
the status would be repeatedly sent.
- In tests strings containing error responses were not valid JSON.
- Workaround Mastodon a bug and ensure `filter.keywords` is populated,
https://github.com/mastodon/mastodon/issues/29142
Previously, `AboutActivity` had buttons and links to show the privacy
policy and licenses of dependencies.
Change this to a selection of fragments in tabs, one tab each for:
- General "About" information
- Licenses
- Privacy Policy
The information shown hasn't changed, but this lays the groundwork for
including additional tabs in the future for information like server
rules, detected capabilities, or troubleshooting information.
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>