Commit Graph

65 Commits

Author SHA1 Message Date
Nik Clayton fc81e8bad7
fix: Ensure actions happen against the correct status (#373)
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
2024-01-26 12:15:27 +01:00
Nik Clayton dcc2954148
fix: Show correct trending tag values at the end of the chart lines (#380)
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.
2024-01-25 00:50:50 +01:00
Nik Clayton 5cfe6d055b
fix: Improve parsing of Friendica (and other server) version formats (#376)
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
2024-01-23 20:27:25 +01:00
Nik Clayton 42219875e9
fix: Disable filter functionality if unsupported by the server (#366)
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
2024-01-18 21:44:30 +01:00
Nik Clayton 098983f401
fix: Calculate length of posts and polls with emojis correctly (#315)
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>
2023-12-12 16:53:09 +01:00
Nik Clayton 1214cf7c8a
refactor: Break navigation dependency cycles with :core:navigation (#305)
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
2023-12-07 18:36:00 +01:00
Nik Clayton 2ce80c6a32
refactor: Use the correct package for TimelineKind (#303)
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`.
2023-12-06 12:20:36 +01:00
Nik Clayton e749b362ca
refactor: Start creating core modules (#286)
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.
2023-12-04 16:58:36 +01:00
Nik Clayton f9fb0e87b4
fix: Prevent UnsupportedOperationException in PachliTileService (#288)
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.
2023-12-02 14:24:29 +01:00
Nik Clayton fce34cced1
fix: Update themes to correct poll/black theme issues (#255)
`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.
2023-11-15 15:59:01 +01:00
Nik Clayton d40b87f0a0
feat: Translate statuses on cached timelines (#220)
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>
2023-11-12 19:51:46 +01:00
Nik Clayton 86dee94035
refactor: Convert Java to Kotlin (#235) 2023-11-06 20:16:34 +01:00
Nik Clayton c350d17646
refactor: Remove additional unused code after migrating to Timber (#219) 2023-11-05 14:50:23 +01:00
Nik Clayton f8877909ca
refactor: Log with Timber (#218)
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.
2023-11-04 22:22:44 +01:00
Nik Clayton 6aa4eab75d
fix: Remove progressbar from status timelines (#208)
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
2023-10-30 19:26:40 +01:00
Nik Clayton 523efa705c
fix: Prevent potential crash when filters are slow to load (#205)
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.
2023-10-28 19:54:46 +02:00
Nik Clayton 2f3851acee
refactor: Convert Java viewholders to Kotlin (#200) 2023-10-26 16:22:18 +02:00
Nik Clayton d39eb3b642
refactor: Extract PreviewCard display code to `PreviewCardView` (#184) 2023-10-19 12:54:58 +02:00
Nik Clayton db2bd3199e
refactor: Create repositories for preferences and StatusDisplayOptions (#149)
Previously, code for handling shared preferences, and how those
preferences affect `StatusDisplayOptions`, was scattered through the
code base with duplicate implementations.

Bring it together in to a `SharedPreferencesRepository` and a
`StatusDisplayOptionsRepository`.

`SharedPreferencesRepository` is a thin wrapper over`SharedPreferences`
that delegates most work to `SharedPreferences`. It configures a
listener for preference changes, and exposes those changes as a flow.

`StatusDisplayOptions` now contains explicit defaults to ensure they
are in one place.

`StatusDisplayOptionsRepository` exposes a `StatusDisplayOptions` flow
that updates whenever the active account changes or a relevant
preference changes.

The viewmodels expose `StatusDisplayOptionsRepository.flow` to the
activities and fragments so they can pass the current value to the
adapter.

This obsoletes `PreferenceChangedEvent`. An event is still fired when
filters change, `FilterChangedEvent`.

This allowed many of the mocks in tests to be replaced with either the
real type (because a fake is injected in to it, or one of its
dependencies) or a custom fake that provides a mock.
2023-10-15 22:52:47 +02:00
Nik Clayton c50f10a989
refactor: Extract Poll display code to `PollView` (#177) 2023-10-15 22:26:34 +02:00
Nik Clayton 71df6254ef
fix: Show thread indicators and other dividers using Material colours (#157)
Use the Material colour for `conversation_thread_line` (which is
`colorOutlineVariant`) instead of a custom attribute.

Elsewhere, use the Material attribute directly (in code), or replace the
custom divider with a `MaterialDivider`.

This makes some colour definitions unused, so remove them.

Fixes #148
2023-10-13 11:36:05 +02:00
Nik Clayton 53e7842439
change: Increase compileSdk and targetSdk to 34 (#150)
Associated changes:

- Handle new null/non-null type signatures in overriden methods
- Configure Robolectric to use SDK 33 (current highest supported
version)
2023-10-11 12:28:45 +02:00
Nik Clayton 38214648dd
refactor: Migrate from Dagger to Hilt (#143)
- Remove `Injectable` interface, use `@AndroidEntryPoint`
- Remove `DispatchingAndroidInjector`
- Remove `viewModelFactory`, use `@HiltViewModel`
- Create providers for the different DAOs, and inject those instead of
  `AppDatabase`
- Create provider for a database transaction, inject that instead of
  `AppDatabase`
- Update tests
2023-10-07 19:30:11 +02:00
Nik Clayton 802cdd4c46
feat: Embed the privacy policy in the app (#139)
Instead of linking to the privacy policy embed it in the app as a string
of HTML.

The string is created with a new `markdown2resource` plugin, which
converts `PRIVACY.md` to HTML and generates a Java class with the HTML
content.

Create `PrivacyPolicyActivity` to display the HTML in a `WebView`, and
link to it from `AboutActivity`.
2023-10-03 12:56:30 +02:00
Nik Clayton 651b0efcd6
feat: Link to the privacy policy from "About" (#137)
Google requires an in-app link to the privacy policy.
2023-09-30 13:15:44 +02:00
Nik Clayton 6fedfe54ba
fix: Restore the user's reading position under all circumstances (#133)
The previous code did not always work when the user returned to the app
after a lengthy absence (e.g., overnight).

Instead of restoring by scrolling in `TimelineFragment`, restore by
working with the platform.

Determine the initial page to fetch by looking half a page ahead of the
saved saved status ID, and fetch that status and the page immediately
prior. This seems to match the view's expectations about what will be
immediately available.

Set `jumpThreshold` and `enablePlaceholders` in the `PagingConfig` so
the paging system will jump to the saved status.

Remove the restoration code in `TimelineFragment`.

Fixes #53
2023-09-29 11:10:55 +02:00
Nik Clayton 50d9aedad9
chore(deps): Update to AGP 8.1.1 (#130) 2023-09-27 18:06:14 +02:00
Nik Clayton 11fecb1914
feat: Show vertical scrollbars on scrollable lists (#96)
Display normal Android (i.e., fading) scrollbars when the user scrolls
in lists.
2023-09-26 15:57:35 +02:00
Nik Clayton af7b668476
fix: Enable/disable vote button when the user can/can't vote (#91)
Previously the voting button was always enabled, even if the user hadn't
made a choice.

Disable the button by default, and listen for clicks on the options.
Enable the button whenever one or more options are selected.

Fixes #90
2023-09-23 21:01:33 +02:00
Nik Clayton 2169c91281 fix: Ensure `setLifeOnScrollTargetView` is called when fragment resumes
If you do not do this and the fragment is in a pager then it can be
overridden when another fragment is swiped in to view.
2023-09-23 16:21:05 +02:00
Nik Clayton f45a3df83f refactor: Use resource strings on the hashtag toolbar menu 2023-09-20 19:05:35 +02:00
Nik Clayton f9e5063ce6 fix: Label the header and avatar on the account screen 2023-09-20 19:05:35 +02:00
Nik Clayton 2bcb595777 fix: Label the image on the focus dialog 2023-09-20 19:05:35 +02:00
Nik Clayton 254edf5e6f refactor: Mark the image overlay is not important for accessibility 2023-09-20 19:05:35 +02:00
Nik Clayton 0fadb6f3fd fix: Set the contentDescription for avatars 2023-09-20 19:05:35 +02:00
Nik Clayton acaf2a7d89 refactor: Remove warnings about unclosed resources
In `MediaUploader` the lint warning can be ignored, as the stream is
closed elsewhere.

In the other files `.use` is used to simplify the code and remove
the need for Closeable.closeQuietly (as `.use` catches exceptions that
are thrown when closing).
2023-09-20 19:05:35 +02:00
Nik Clayton d00dc97a5f refactor: Suppress an unncessary CheckResult lint error
The result is used, lint isn't smart enough to figure that out.
2023-09-20 19:05:35 +02:00
Nik Clayton 9de829995b
fix: Check permissions before sending a failure notification (#77) 2023-09-19 22:18:17 +02:00
Nik Clayton 0f6975ffcc
fix: Check build version is >= T before POST_NOTIFICATIONS request (#76) 2023-09-19 22:04:42 +02:00
Nik Clayton 0bf459d385
refactor: Use "compat" drawables where appropriate (#72)
Use AppCompatResources.getDrawable() and app:drawableStartCompat.
Resolves existing lint issues.
2023-09-19 17:42:20 +02:00
Nik Clayton c97c3a4156
refactor: Add @NonNull and @Nullable annotations where appropriate (#71)
Adding the annotations cleans up an entire class of lint errors, and
it will be easire to convert from Java to Kotlin later.
2023-09-19 17:17:31 +02:00
Nik Clayton f4e14dcf44
fix: Restore the user's reading position in more circumstances (#49)
The previous code only attempted to restore the user's reading position
once, after any initial refresh.

Adjust this so the position is restored after any refresh (which may
have been triggered from a menu instead of a swipe), and use
`scrollToPositionWithOffset` to ensure it's visible.
2023-09-15 16:09:18 +02:00
Nik Clayton 7bb1e6fe17
change: Restore dividers between list items (#45)
Switching to the Material 3 themes caused the previous list dividers to
disappear.

Replace `DividerItemDecoration` with `MaterialDividerItemDecoration` to
restore them.
2023-09-14 19:10:59 +02:00
Nik Clayton ec66942ae9
fix: Show the FAB according to the user's preferences (#29)
The previous code didn't collect the uiState, so it was fixed at the
default value, ignoring any changes that happened over the life of
the viewmodel.

Fix that, so that the FAB will hide/show on scroll according to the
user's preferences.

While I'm here simplify the show/hide logic. The previous code would
ignore the user's preference if scrolling up. There doesn't seem to
be a good reason for that, and spelunking 6+ years back through the
history didn't find a justification for that behaviour in the original
commit.

Fixes #15
2023-09-11 21:19:45 +02:00
Nik Clayton ecd81e80b0
fix: Fix toolbar flickering when scrolling lists (#26)
Scrolling a thread, set of search results, or viewing a thread would
cause the toolbar to flicker as items moved under it.

Fix this by configuring the toolbar to `liftOnScroll` in the relevant
layouts.

It needs to be configured with the view (or ID of the view) that it
will be scrolling. For views that are in the same layout this is done
with the `liftOnScrollTargetViewId` attribute.

For views that are in different layouts (e.g. the toolbar is in
the activity and the scrolling view is in a fragment) the app bar's
`setLiftOnScrollTargetView` method must be called.

Do this in `TimelineFragment` if the hosting activity is a new
interface `AppBarLayoutHost`. Implement this interface in
`StatusListActivity`.

Update the relevant layouts to use `MaterialToolbar`.

Fixes #21
2023-09-11 17:48:42 +02:00
Nik Clayton 811856a3b6
fix: Fix crash on entering MainActivity on Pixel C devices (#25)
First crash appeared to be caused by a failure to find the
`attr/colorBackgroundAccent` colour from the theme.

It wasn't clear why the attribute could not be found, so to fix it was
simpler to remove the color and attribute entirely, and replace it with
something more appropriate from the Material 3 tokens.

- Preview cards are stroked with `colorOutline`
- Poll options use `colorPrimary` (user's vote) or `colorSecondary`
  (other choices) with appropriate text colours.
- Links in link preview cards use `android:attr/textColorLink`
- The placeholder icon in preview cards uses `?android/textColorLink`
- Remove it from `help_message_background`, and stroke with
  `?colorOutline`

Doing this I discovered several places where a colour was being
specified unnecessarily, those have been removed.

To make it easier to understand the theme hierarchy that has been
collapsed and renamed to follow Android conventions.

- AppTheme -> Base.Theme.Pachli
- BaseTheme -> Theme.Pachli
- DefaultTheme has been removed as unnecessary

This unearthed a second crash, where `attr/actionBarSizeWithSubtitle`
was not found.

To fix that create an explicit style for toolbars that need it, and
apply the style (`Pachli.Widget.Toolbar`).

This also surfaced a third problem, where the `fragment_timeline*`
layouts had not been updated in `layout-sw640dp`, so those have been
updated to reflect the same views/IDs as the default `fragment_timeline`
layout.

These changes caused a small chain of "unused resource" lint errors,
which have been fixed by removing the unused colours.

The Android Material libraries were also being implicitly depended on
through other library imports instead of being explicit. So include
them as an explicit dependency.

Fixes #18
2023-09-11 13:54:29 +02:00
Nik Clayton 4879f0449f
docs: Add a privacy policy (#2)
While drafting the policy I noticed that the `READ_MEDIA_*` permissions
could be added (for newer devices), the `ACCESS_NETWORK_STATE`
permission was missing, and `VIBRATE` was unnecessary.
2023-09-05 15:35:06 +02:00
Nik Clayton 1bf13b10f8
refactor: Transition from Tusky to Pachli
- Rename packages to app.pachli.*
- Switch to Pachli icons (blue / orange)
- Reset database schema version to 1
- Reset versionCode to 1 and versionName to "1.0"
- Update colour scheme, use colorPrimary etc through the app
- Use Material UI components for toolbars
- Use "Pachli" in strings (UI, constants, etc)
- Update copyright on code I contributed
- Update README
- Update fastlane metadata
2023-09-05 13:33:37 +02:00
Nik Clayton a441576bf6
style: Require trailing comma, and break lines
Requiring trailing commas on multi-line lists of items (declarations
and call sites) reduces future repository churn when those lines are
changed, but introduces additional churn now.

Bite the bullet and make the change, as well as adjusting lines that
were too long / indented incorrectly.

The changes were performed automatically, using the `ktlintFormat` task.

Based on https://github.com/tuskyapp/Tusky/pull/3968 by
https://github.com/tinsukE
2023-09-04 20:22:10 +02:00
Nik Clayton fc2a830ea1
feat: Remove explicit "Load more", load on demand
Prior to this change the user had to repeatedly tap "Load more" when
scrolling. This is tedious for the user.

In addition, the previous code had bugs that meant that not all statuses
were being loaded. Users could leave the app for a while (overnight,
say), and when returning would discover far fewer statuses than had
actually been posted.

Fix this, following the architecture first introduced for notifications
(Fragment -> ViewModel -> Repository -> Source/Mediator).

- Load statuses for cached and non-cached timelines using Paging3
- Show Failures during a load, and the user can retry
- Delete the "Reading order" preference, it is no longer necessary
2023-09-04 20:22:08 +02:00