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.
Previous code didn't trigger the transition from `ViewMediaActivity`
when playing a video until the video had loaded. If the connection was
slow or had other issues this resulted in the video "sticking" in the
timeline until it loaded.
Change this, and trigger the transition immediately.
Fixes#598
While looking at this:
- Save the play/pause state of the video during a swipe, pause the
video, and restore the state when the swipe is cancelled.
- Transition the entire video view, to improve the animated transition
back to the activity.
- Remove the custom progress spinner, use the one provided by the
player.
- Display the player controller via the layout XML instead of code
The previous code didn't set a limit for the number of posts, links, and
hashtags to fetch on the trending pages, so used the conservative
defaults.
Increase these to the API maximums to show the user more information.
The PageCache implementation wasn't properly dealing with timelines that
could return statuses in non-chronological order.
For example, if you bookmark a recent status, then go back in the
timeline and bookmark an older status; the bookmarks timeline is ordered
by the time of the bookmark event, not the creation time of the status
that was bookmarked.
If a sufficiently old status was bookmarked so it straddled a page
boundary you could have a situation where the range of status IDs in two
different cached pages overlapped.
E.g., this log extract:
```
0: k: 110912736679636090, prev: 3521487, next: 3057175, size: 40, range: 112219564107059218..110912736679636090
1: k: 111651744569170291, prev: 3049659, next: 2710596, size: 40, range: 111926741634665808..111651744569170291
```
The range of IDs in page 0 overlaps with the range of IDs in page 1.
The previous `PageCache` assumed this couldn't happen, and broke in
various interesting ways when it did.
E.g., you can't find the page that contains a given status by looking
for the largest key less than the needle's status id. Given the pages
above looking for ID 112219564107059218 (first status in page 0) would
suggest page 1 as having the greatest key less than that ID. This
manifested as the correct page briefly appearing in the UI (page 0),
then being completely replaced with page 1.
Rewrite PageCache to fix this. The previous implementation used a single
`TreeMap` assuming items were always sorted by ID. The new code keeps an
unordered map from status IDs to the page that contains that status, and
a separate `LinkedList` that contains the pages in order they're
provided by the API. This decouples the ordering of pages in the cache
with the overall ordering of items within the pages.
https://github.com/pachli/pachli-android/pull/589 changed the initial
setting of the action bar title to use `binding.mainToolbar.title`
instead of `supportActionBar?.title`.
Not sure why, but this doesn't work on first use, and was showing
"Pachli Current" until the user changes tabs. Swap one usage back to
`supportActionBar?.title` to fix this, and update the other to do the
same thing to keep the code consistent.
Use `unicodeWrap` when inserting placeholders in error messages so they
set the correct text direction.
Update some strings with formatting directives to (a) include `_fmt`
in the name, and (b) use `%1$s` instead of `%s`.
The compose button isn't initially working on `MainActivity`. It is only
properly setup after changing tabs.
Fix by calling `MainActivity.refreshComposeButtonState(TabViewData)` on
creation.
Previously, modifying any tabs meant opening the left-side nav, opening
Account preferences > Tabs, and then adding / removing tabs. This is
time consuming, and difficult for new users to discover.
In addition, it was possible to remove the Home tab, and there was a
hardcoded minimum of at least two tabs.
Fix this.
When viewing a timeline that is not already in a tab an "Add to tab"
menu item is enabled, which appends the timeline to the list of existing
tabs.
When viewing a timeline in a tab (that is not the Home timeline) a
"Remove tab" menu item is enabled, which removes the tab from the list
of existing tabs.
If the user removes the active tab (either with this menu item, or
through preferences) the tab to the left of the active tab becomes the
new active tab.
A new "Manage tabs" menu item is also provided, as a shortcut to the
existing Account preferences > Tabs screen.
When managing tabs the Home timeline can not be removed; the button to
remove it is removed, and swiping is disabled on that list item. The
restriction of "at least two tabs" has also been removed.
`NotificationsActivity` has been removed, as `TimelineActivity` can
display `NotificationsFragment`.
To make the three "Trending" types (hashtags, links, and posts) more
visually distinct add two new icons for links (ic_newspaper) and posts
(ic_whatshot).
Fixes#572, #584, #585, #569
Mastodon API uses an "empty" `expires_in` value for a filter to mean
"Does not expire" (i.e., indefinite).
This was modelled as a null. Which doesn't work, because Retrofit does
not send name/value pairs in encoded forms if the value is null.
Fix this by making the API type a `String?`, and explicitly using the
empty string when indefinite expiry is used. This has to be converted
back to an Int? in a few places.
See
https://github.com/mastodon/documentation/issues/1216#issuecomment-2030222940
Previous code was inconsistent about how and when the FAB was hidden if
the user had set the relevant preference.
- Sometimes the FAB has hidden by setting the visibility to false, which
removed it with no animation.
- Sometimes the value of the preference was checked once, when the
fragment or activity was created.
- Some timelines didn't show the FAB (Hashtags, Favourites, Bookmarks,
TrendingLinks, TrendingStatuses).
- Logic for figuring out which `ComposeActivity` intent to use was
scattered across different files.
Improve this by:
- Expose changes to the `FAB_HIDE` preference in the relevant viewmodels
as a flow the UI component can collect.
- Centralise the show/hide logic in a new `ActionButtonScrollListener`
class, and always using `show()`/`hide()` to animate the transition.
- Centralise the logic for creating the `ComposeActivity` intent in
`TabViewData`.
`TabData` recorded the type of the timeline the user had added to a tab.
`TimelineKind` is another type that records general information about
configured timelines, with identical properties.
There's no need for both, so remove `TabData` and use `TimelineKind` in
its place.
`TimelineKind` is itself mis-named; it's not just the timeline's kind
but also holds data necessary to display that timeline (e.g., the list
ID if it's a `.UserList`, or the hashtags if it's a `.Hashtags`) so
rename to `Timeline` to better reflect its usage. Move it to a new
`core.model` module.
Workaround a Glide bug where the error() handler is not always called,
in this case when the URL does not resolve to an image; for example, a
misconfigured server that redirects requests for the image to an HTML
page.
Catch the exception and use the default avatar image in these cases.
Previous code would handle some expected exceptions (IO, HTTP) when
fetching a timeline, and show them to the user. Any other exception
would crash.
Now, surface all exceptions. Treat IO and HTTP exceptions as retryable
and show the "Retry" option, all others are considered non-retryable.
Provide a specific error string for exceptions caused by bad JSON.
Previous code used custom regular expressions to extract URLs, hashtags,
and mentions from text while the user was writing a post. These were
inconsistent with the ones that Mastodon uses so the derived character
count could be wrong.
As well as being visually incorrect this could prevent the user from
posting a status that was within the length limit, or allow them to
attempt to post a status that was over the length limit (which would
then fail).
Fix this by dropping the homegrown regular expressions and using the
same text parsing library that Mastodon users; twitter-text. This has
been converted to Kotlin and the functionality related to Twitter
specific features has been removed.
The hashtag handling has been adjusted, as Mastodon is more permissive
about the positions where hashtags can appear than Twitter is, in
particular, a hashtag does not need to be preceded with whitespace if
the tag appears after some scripts, such as Hirigana.
The previous bottomsheets did set a minimum height for the menu items,
so they were less than the recommended 48dp minimum. Fix that to improve
the overall accessibility.
Always highlight the "visibilty" icon, to make it clear that it's
something that is set (even if to the default).
Show the visibility icon on the "Toot" button as an additional reminder
to the user.
Other changes:
- Use the "filled" style for all icons (the visibility icons had the
"outlined" style)
- Use the `makeIcon` helper function.
- Use the `Status.Visibility` extension functions to determine the icon
for each visibility type, reducing code duplication.