Previous code showed any JSON-wrapped errors from notification fetches
as the JSON string, instead of the error message.
Fix this by switching to `ApiResult` and using the formatted error
message.
Fixes 937
Previous code saved the reading position of a fully visible status. But
there are situations where no status is fully visible.
1. The user is in the middle of viewing a status longer than the screen
height, and the top/bottom of the status are off the top/bottom of the
screen.
2. The user has scrolled between two statuses. Collectively they are
longer than the screen height, and the top of one status is off the top
of the screen and the bottom of the other status is off the bottom of
the screen.
In both cases the user's reading position was not saved.
In these situations use the ID of the status closest to the bottom of
the screen, even if not fully visible. This should ensure the user never
missing anything.
Fixes#936
Previous code used `Response`. Convert to `ApiResult` as part of the
work to implement anti-harassment controls, which will need to query the
user's list of accounts they are following.
Converting just `accountFollowing` wasn't practical, as all the methods
are called by a single function in `AccountListFragment` which expects
the return type to be the same.
In rare occasions the preview card text could overlap the image if the
image had a portrait aspect ratio.
This seems to be due to the use of the `with(...) {}` scope function and
Kotlin's interoperability with Java setters.
Replace this with code that explicitly gets and sets the layout params
to ensure they are set correctly.
androidx.media3 1.4.0-rc01 and above (at the time of writing) has a bug
that breaks shared element transitions with a `PlayerView` (see
https://github.com/androidx/media/issues/1594).
This can be worked around by setting `app:surface_type="texture_view"`.
This uses more power, but for the typical length of social media videos
this shouldn't be a problem at the moment.
Fixes#920.
Previous code used `filterIsInstance<Ok<UploadEvent.FinishedEvent>>()`.
This can fail at runtime with class cast exception because the type in
`Ok<...>` is erased so `filterIsInstance` was accepting any `Ok`
`Result`. Later attempts to operate on it as a `.FinishedEvent`
generated the run time error.
Fix that by explicitly checking the type of the `Ok` result in `first`
instead.
Previous code had a bug/typo, which meant the app the user was posting
from was not shown if the app did not have an associated website. But
the bullet separating the parts of the text was still shown, resulting
in a spurious dangling bullet.
Previous code didn't set the textDirection for the status content, so
the first para of RTL text might be rendered incorrectly.
In addition, mentions and tags weren't BIDI wrapped, so would appear as
"foo@" and "foo#" in RTL statuses, instead of "@foo" and "#foo".
Fix both of these issues.
Fixes#870
Two problems with the previous code when search filters were visible:
1. The link icon overrode the tint, so didn't appear correctly in dark
and black mode.
2. The horizontal scroll view had the wrong background colour in black
mode.
Fix both, by updating the icon and adding a new style for the scroll
view.
While I'm here remove an obsolete comment and tighten up visibility.
Fixes#875
Clean up the notification handling code and fix a lot of bugs, hopefully
without introducing new ones in the process.
Specific bugs discovered and fixed:
- The code that tried to sync notification filtering state between the
server and Pachli could fail, leaving things in an inconsistent state,
resulting in dropped notifications. Remove that code, do filtering
client-side.
- Logging out of an account would disable push notifications for all
accounts.
- If any account did not support push notifications then push
notifications were disabled for all accounts.
- If any account did not support push notifications the user was
prompted to log out of all accounts. Drop that entirely.
- The UnifiedPush library could get to a state where configuring the
notification mechanism would silently fail,
The preferences UI now has a section for notifications, showing:
- The Unified Push distributor in use (if any)
- A mechanism to change the distributor
- Per-account configuration and notification fetch details
- Battery optimisation state
General changes:
- Update to UnifiedPush library 2.4.0.
- NotificationFetcher.fetchAndShow() can now fetch a single account's
notifications, or all accounts, depending on data passed to the worker.
- Use ApiResult for `push/subscription` responses.
- Drop the "needs migration" terminology for the more specific "has push
scope", to make it clear what the issue with the account is.
Add a new set of preferences, "Lab experiments", to control features
that are under investigation and may never make it into the mainstream.
Add the first experimental feature, which reverses the order of the home
timeline, so posts are shown oldest first instead of newest first.
Byline changes inadvertently changed how the preview image is laid out,
breaking the "Image at start, info at end" variant.
Previous code did not always show the card description if the text was
present, fix that.
Previous code set a min-height, which is no longer necessary after the
other layout changes. But it meant that a preview card with a one-line
title, no synopsis, and a URL, was taking up too much vertical space.
Previous code displayed a large placeholder icon if there was no preview
image for a preview card.
This reduces the amount of space available for the actual preview text
(i.e., title and description) and did not convey additional information
in the limited space available on the timeline.
So remove it.
While I'm here simplify the PreviewCard layout and migrate to
ConstraintLayout.
The `canFilter()` implementation could crash if `server` (marked
`lateinit`) hadn't been initialised at the point of use.
Fix this by removing it and adjusting the two callers to use the
`filters` flow and take appropriate action on error.
Default to hiding the search operators, and provide a new toolbar icon
(always visible) to show them.
The toolbar icon is displayed with a badge if any operators are present.
Adjust the operator display to three horizontal scrolling rows, to
further limit the maximum amount of vertical space the operators use.
Previously re-binding the tag's text wouldn't re-measure the layout when
the text changed. So scrolling down a longer tag could be placed in a
textview that previously held a shorter tag.
Then it was cut off and the user couldn't see what the tag was. Use
`wrap_content` so size is measured when the content changes.
Mastodon supports in-query search operators, such as `has:image`,
`language:en`, or `in:library`. Previously the user had to enter them in
to the query directly.
This provides a chip-based UI that allows the user to set values for
these operators.
## Server
- Add new search capabilities to record the faceted search features the
server reports.
- Update definitions for Mastodon, Friendica, and GoToSocial to specify
which versions of the operations they support.
## SearchOperator / SearchOperatorViewData
- Represents each supported operator and associated viewdata.
## SearchActivity / activity_search.xml
- Conditionally display a chip for each facet depending on the server's
level of support.
- Implement the UI for each chip. They display dialogs of varying levels
of complexity depending on the underlying operation.
## FragmentSearch
- Display the progress as a LinearProgressIndicator instead of an
indeterminate ProgressBar. This makes it more visible under the search
facets.
Previously, if a status was filtered with "WARN" and was shown in the
timeline with the name of the filter, and the user then decided to
change
that filter, they had to:
1. Open the left navigation menu
2. Navigate to "Account preferences"
3. Open "Filters"
4. Find the filter they want to edit, tap it
5. Make the change, and save
6. "Back" to the list of filters
7. "Back" to "Account preferences"
8. "Back" to the timeline
That's a lot of clicks for a simple action.
Change this. Now the filtered status includes an "Edit filter" button
that takes the user directly to step 5, and when they press "Back" they
return directly to the timeline.
To do this create a new filter action, `onEditFilterById`. Update the
listeners to launch `EditFilterActivity` if appropriate.
Modify `item_status_filtered.xml` to show the new button.
Update the accessibility delegate to show just the "Show anyway" and
"Edit filter" actions. Modify `FilterableStatusViewHolder` to expose
the information it needs to do this.
Previous code always focused the search query. This meant that if the
user:
1. Searched for something
2. Opened a result (post, hashtag, account)
3. Navigated back to the search results
then because the query was focused the soft-keyboard would open,
obscuring the list of results. The user had to press "Back" again to
dismiss the keyboard.
New code only focuses the search query view if it is empty. This allows
the user to come back to the list of results and immediately open a new
result.
Previous code set `doOnTextChanged` listener for the content warning
*after* the initial value had been set. This meant the initial content
warning text was not included when calculating the status' initial
length.
Fix that by setting the listener before the text is set.
Fixes#815
The previous code had a number of problems, including:
- Calls to the filters API were scattered through UI and viewmodel code.
- Repeated places where the differences between the v1 and v2 Mastodon
filters API had to be handled.
- UI and viewmodel code using the network filter classes, which tied
them to the API implementation.
- Error handling was inconsistent.
Fix this.
## FiltersRepository
- All filter management now goes through `FiltersRepository`.
- `FiltersRepository` exposes the current set of filters as a
`StateFlow`, and automatically updates it when the current server
changes or any changes to filters are made. This makes
`FilterChangeEvent` obsolete.
- Other operations on filters are exposed through `FiltersRepository` as
functions for viewmodels to call.
- Within the bulk of the app a new `Filter` class is used to represent a
filter; handling the differences between the v1 and v2 APIs is
encapsulated in `FiltersRepository`.
- Represent errors when handling filters as subclasses of `PachliError`,
and use `Result<V, E>` throughout, including using `ApiResult` for all
filter API results.
- Provide different types to distinguish between new-and-unsaved
filters, new-and-unsaved keywords, and in-progress edits to filters.
## Editing filters
- Accept an optional complete filter, or filter ID, as parameters in the
intent that launches `EditFilterActivity`. Pass those to the viewmodel
using assisted injection so the viewmodel has the info immediately.
- In the viewmodel use a new `FilterViewData` type to model the data
used to display and edit the filter.
- Start using the UiSuccess/UiError model. Refrain from cutting over to
full the action implementation as that would be a much larger change.
- Use `FiltersRepository` instead of making any API calls directly.
## Listing filters
- Use `FiltersRepository` instead of making any API calls directly.
## EventHub
- Remove `FilterChangedEvent`. Update everywhere that used it to use the
flow from `FiltersRepository`.
Previous code blindly inserted commas and semi-colons as separators
between the components of a content description. If some of those
components were null you could have a content description that looked
like "... , , , ..." or similar, and the repeated reading of "comma" by
screen readers was jarring and reduced accessibility.
Fix this by inserting punctuation only where necessary, building up the
string piece by piece instead of using a string resource with hardcoded
punctuation.
Fixes#791.
When autocompleting hashtags while composing a status the previous code
showed the hashtags in the same order they're returned by the server,
with no additional information.
This doesn't allow the user to make an informed choice about which
hashtag might be better to use. For example, trying to choose between
"#nivenly" and "#NivenlyFoundation".
To fix that, include the hashtag's usage when receiving data from the
server. Sum that, and show it to the user in the hashtag list. Sort the
hashtags by popularity, most popular first.
Mastodon now supports additional (optional) author information to show
as a byline on preview cards.
Use this (if included), to show the author's avatar, name, and link to
their profile. If tapped a click on a new `Target.BYLINE` target is
registered allowing fragments/activities to launch
`ViewProfileActivity`.
Include this as an action in `ListStatusAccessibilityDelegate`, and
provide `TrendingLinksAccessibilityDelegate` to provide accessibility
actions when viewing trending links.
Previous code assumed server responses would always be JSON, and had no
special handling for mis-configured servers that sometimes return HTML;
for example, if the server has a bug, or there's a reverse proxy in
front of the server issuing DoS-prevention challenges.
This could cause errors to show with no useful debugging information.
Update `ApiResult` to check the content-type in the response and return
one of two new errors if the content-type is missing or wrong. Also
include the HTTP code in `ApiResponse` for use elsewhere.
Update `ThrowableExtensions` to pull the `error` and optional
`description` out of the error body.
Update `PachliError` so `formatArgs` can be an array of arbitrary types,
not just strings.
Update `MediaUploader`; expose the different errors as new
`MediaUploaderError` types instead of `Exception` subclasses, and return
`Result<V, E>` where appropriate.
Update `ComposeViewModel` to use the new `MediaUploaderError` types and
create new `PickMediaError` to report issues there, replacing
`VideoOrImageException`.
Update `ComposeActivity` to use the new error types and show errors
until the user dismisses them, so they're better able to see and report
problems.
Fixes#704.
The user has to specify the language they're posting in, and sometimes
they might get it wrong (e.g., replying to a post that also had the
language set incorrectly, forgetfulness, etc).
This has accessiblity issues (only following statuses in a given
language fails, translation can fail, etc).
Prevent this by trying to detect the language the status is written in
when the user tries to post it. If the detected language and the set
language do not match, and the detection is 60+% confident, warn the
user the status language might be incorrect, and offer to correct it
before posting.
How this works differs by device and API level.
- API 23 - 28, fdroid and github build flavours
- Not supported. A no-op language detector is used.
- API 29 and above, fdroid and github build flavours
- Uses Android TextClassifier to detect the likely language
- AP 23 and above, google build flavour
- Uses ML Kit language identification
To do this:
- Add `LanguageIdentifier`, with methods to do the identification, and
`LanguageIdentifier.Factory` to create the identifiers.
- Inject the factory in `ComposeActivity`
- Detect the language when the user posts, showing a dialog if there's a
sufficiently large discrepancy.
The ML Kit dependencies (language models) will be installed by the Play
libraries, so there's some machinery to check that they're installed,
and kick off the installation if not. If they can't be installed then
the language check is bypassed.
Update the privacy policy, as the ML Kit libraries may send some data to
Google.
This reverts commit 3870267701.
With this commit Pachli Current crash reports in Google Play showed
instances of:
```
Exception java.lang.RuntimeException:
at android.app.ActivityThread.handleBindApplication (ActivityThread.java:7716)
at android.app.ActivityThread.-$$Nest$mhandleBindApplication
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2478)
at android.os.Handler.dispatchMessage (Handler.java:106)
at android.os.Looper.loopOnce (Looper.java:230)
at android.os.Looper.loop (Looper.java:319)
at android.app.ActivityThread.main (ActivityThread.java:8919)
at java.lang.reflect.Method.invoke
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:578)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1103)
Caused by java.lang.IllegalStateException: WorkManager is not initialized properly. You have explicitly disabled WorkManagerInitializer in your manifest, have not manually called WorkManager#initialize at this point, and your Application does not implement Configuration.Provider.
at androidx.work.impl.WorkManagerImpl.getInstance (WorkManagerImpl.java:170)
at androidx.work.WorkManager.getInstance (WorkManager.java:184)
at app.pachli.PachliApplication.onCreate (PachliApplication.kt:96)
at android.app.Instrumentation.callApplicationOnCreate (Instrumentation.java:1316)
at android.app.ActivityThread.handleBindApplication (ActivityThread.java:7711)
```
on Samsung devices at API 34.
By my understanding this is a "can't happen" issue, the `WorkManager` is
supposed to be initialised by the androidx.startup content provider
before `PachliApplication` starts.
But it clearly does, so revert this change to be safe.
Implementing predictive back support inadvertently broke the code
ensuring activities are restarted when the font size or family changes.
So the user could change the font in Preferences, see the change be
immediately reflected in the preferences screen, then go back to the
previous activity and the change wouldn't be reflected.
Fix this by restoring some of the previous code.
Previous code set the initial status text, and then set up the callbacks
which meant that the status' length was initially 0, even when editing a
status.
This meant that, e.g., editing a status to change its language would
erroneously report the status body was empty. It also meant that editing
a status and changing just the language would not prompt to save or
discard the changes if moving back.
Fix this.
First, only set the status content after the callbacks that compute the
status length.
Second, provide a function that sets the status' language, and update
the close confirmation state when the language changes. Modify isDirty()
to compare the original and current language when determining if the
status is dirty.
Fixes#701
Previous code did not provide whitespace between different media labels
when media is not loaded.
In addition, the icon for the media was centre-aligned vertically with
the text, making it difficult to scan and determine when one media label
ends and another one starts.
Fix this by adding an 8dp margin between the media labels, and using a
TextView subclass that vertically aligns the media icon with the first
line of text.
Set the compound drawables with relative alignment, so they behave
appropriately in RTL layouts.
Fixes#751.
The user might have set a profile header image that is close to the
colour of the text in the account header in the left navigation menu.
This can make the text difficult or impossible to see.
Work around this by drawing a partially transparent scrim behind the
text so it's always displayed over a background that makes the text
legible.
Fixes#298
Previous code created one shortcut per account, which could exceed the
maximum number of shortcuts allowed, causing a crash.
Fix this by creating no more than the max number of shortcuts while
ensuring that the active account is always included.
Fixes#752
`ImageDownsizer.downsizeImage()`:
- Remove the return value, it was ignored
- Throw `FileNotFoundException` when `openInputStream` returns null
`ImageDownsizer.getImageOrientation()`:
- Throw `FileNotFoundException` when `openInputStream` returns null
`MediaUploader.prepareMedia()`:
- Copy URI contents using Okio buffers / source / sink
`UriExtensions`:
- Rename from `IOUtils`
- Implement `Uri.copyToFile()` using Okio buffers / source / sink
- Replace `ProgressRequestBody()` with `Uri.asRequestBody()` using Okio
buffers / source / sink
`DraftHelper.copyToFolder()`
- Use Okio buffers / source / sink
`CompositeWithOpaqueBackground`
- Use constants `SIZE_BYTES` and `CHARSET` instead of magic values
- Use `Objects.hash` when hashing multiple objects
Based on work by Christophe Beyls in
- https://github.com/tuskyapp/Tusky/pull/4366
- https://github.com/tuskyapp/Tusky/pull/4372
Previous code depended on, but did not initialise, the androidx
splashscreen library.
Fix that, using the library on API < 31, or the platform implementation
otherwise.
`SplashActivity` is no longer needed, launching goes straight in to
`MainActivity`.
Version 1.2.0-alpha01 is needed to fix some theme corruption bugs in
earlier versions of the library.
Previous layout set `navigationContentDescription` to
`action_open_drawer`, which is not correct for that part of the UI.
Remove it, and the default "Navigate up" is used.
Move initialisation of WorkManager and Timber in to new `Initializer`
classes, managed by `androidx.startup`.
There's a false-positive `BadConfigurationProvider` lint message because
the `Configuration.Provider` is in the content provider and not the
application class.
New lint checks mean:
- Some trivial uses of String.format() are replaced with templates
- Use a string resource for the scheduled date and time
Some reported SetTextI18n warnings have been ignored, as they relate to
e.g., clearing a view's text by setting it to "".
Implement suggestions as a new `feature:suggestions` module, with
associated activity, fragment, etc.
Suggested accounts are shown with their normal information, as well as
information about the number of follows / followers, and a guide to
posting frequency, so the user can make a more informed decision about
whether to follow or not.
In the previous code `PachliError` could correctly chain errors and
generate error messages, `ApiError` didn't, which is why there was the
temporary `ApiError.fmt()` extension function.
Rewrite `ApiError` to implement `PachliError` so it gets these benefits
and to reduce the number of different error-handling mechanisms in the
code.
Main changes:
- `PachliError` is now an interface so it can be extended by other
error interfaces.
- All the `ApiError` subclasses implement `PachliError`, and can
specify the error string and interpolated variables at the point of
declaration.
- Update `ListsRepository` and `ServerRepository` to return
`PachliError` subclasses.
The crash was caused by an uncaught exception when calling
`MastodonApi#domainBlocks`.
Update the `BackgroundMessageView` for the error message so it fills the
screen like on other pages.
Fixes#696
Previous code was inconsistent about using getServerErrorMessage() and
whether or not the case where getServerErrorMessage() returns null was
handled.
Switch to using getErrorString() which does handle the null case and
always returns a usable string.
Some string resources are rendered temporarily unused by this change.
They will be used again soon, so configure lint to ignore them at the
moment.
Previous code always included the transitionKind enum as an extra, and
could cause a crash if the activity launched by the intent was not a
Pachli activity.
Fixes#700.
The F-Droid API can return a `suggestedVersionCode` that F-Droid has not
successfully built (e.g., there was a network issue preventing F-Droid
from fetching the source code at the time of the last build).
This meant users were being prompted to update when there was not a
built package to update to.
Fix this by verifying that `suggestedVersionCode` appears in the list of
packages. If it doesn't then fall back to the highest version code in
the list of packages.
Fixes#684
Previous code didn't get the viewmodel to check if the content had
changed, so `onBackPressedCallback` wasn't enabled, and the user could
edit the bio and press "back" without being prompted to save their
changes.
Edited polls only include the list of options with titles; no other
metadata (poll ID, single/multiple choice, vote counts, etc). Since the
data shape didn't match Moshi wasn't decoding the data.
Provide dedicated data classes to model the response, and add a fourth
poll display option to represent viewing an edit history snapshot.
Replace `ContextCompat.getDrawable()`, as the app-compat version has
platform fixes and backports.
appcompat-lint warns about bare `context.getDrawable()`, but does not
cover `ContextCompat` (https://issuetracker.google.com/issues/337905331)
`ListStatusAccessiblityDelegate` was ignoring notifications because it
was checking the status against the concrete `StatusViewData` and not
the interface `IStatusViewData`.
Fix that and now notifications have accessibility actions.
`ConversationsFragment` didn't set the accessibility delegate, so no
actions appeared. Fix that so they do.
Crash was occuring because the instance info hadn't been fetched, trying
to take the last item of an empty list.
To fix:
- Expose the instance info as a state flow, with a default. New instance
info is fetched whenever the active account changes.
- Do the same for the emojis supported by the server.
- Update call sites as appropriate.
- Mark `InstanceInfoRepository` as `@Singleton` so it isn't repeatedly
created causing fresh content fetches.
The tests needed updating to get this to work.
- Extract the network fake modules in to a network-test module so
multiple other modules can use them.
- Rewrite `InstanceInfoRepositoryTest` to use Hilt and use Turbine to
test the new flows.
Checking this showed cosmetic bugs in the About layout when instance
info is missing, clean those up.
The change in 5959d23d resulted in a stuck margin when scrolling up in
the account view.
Fix the `SwipeRefreshLayout` offset in a different way that doesn't have
that problem; move the spinner's start position to the top of the screen
(instead of the negative offset it normally has) and keep the same
relative offset from the end position.
Have it scale in on the drag to complete the effect.
Previous code used a single animation type (slide) when transitioning,
the transition was quite slow, and didn't behave appropriately if the
device was set to a RTL writing system.
In addition, handling the back affordance didn't work well with the new
"Predictive Back" feature in Android 14
(https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture).
Fix this.
## Transitions
To update the transitions the `startActivityWithSlideInAnimation()`
implementation (and associated `finishWithoutSlideOutAnimation()`) have
been replaced.
There are three transitions; `default`, `slide`, and `explode`,
represented as an enum passed to the activity intent.
The `default` transition is the activity transition from Android 14,
from the Android open source project
(https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:frameworks/base/core/res/res/anim/;bpv=1).
This is used for most transitions.
The `slide` transition is the pre-existing slide transition, with a
shorter transition time so it feels more responsive, and an RTL
implementation. This is used when there is a strong spatial component to
the navigation, for example, when going from:
- a status to its thread
- a preference menu item to its subscreen
- a filter in a list to the "edit filter" screen
- viewing your profile to editing your profile
The `explode` transition is used when the state of the app changes
significantly, such as when switching accounts.
Activities are now started with `startActivityWithTransition()` which
sets the intent and prepares the transition. `BaseActivity` checks the
intent for the transition type and makes further changes to the
transition as necessary.
## Predictive back
"Predictive back" needs to know what the back button would do before the
user interacts with it with an `onBackPressedCallback` that is either
enabled or disabled. This required refactoring some code (particularly
in `ComposeActivity`) to gather data ahead of time and enable/disable
the callback appropriately.
## Fixed bugs
- Back button wasn't stepping back through the tabs in AccountActivity
- Modifying a filter and pressing back without saving wasn't prompting
the user to save the changes
- Writing a content warning and then hiding it would still count the
text of the content warning toward's the post's length
## Other cleanups
- Use `ViewCompat.setTransitionName()` instead of setting the
`transitionName` property
- Delete the unused `fade_in` and `fade_out` animations.
- Use androidx-activity 1.9.0 to get the latest predictive back support
library code
- Show validation errors when creating / editing filters
AccountActivity is full screen, so the swipe spinner was appearing very
close to the top system bar. Adjust the spinner's top margin to avoid
the system bar, and have it scale in/out so it doesn't appear to slide
out from an invisible barrier.