Now that the flavour includes the store name it's not sufficient to
check for "orange" as the flavour, as that no longer matches. Now it
must start with "orange" to trigger using the git commit count as the
version code.
Users can inadvertently get stuck on older versions of the app; e.g., by
installing from one F-Droid repository that stops hosting the app at
some later time.
Analytics from the Play Store also shows a long tail of users who are,
for some reason, on an older version.
On resuming `MainActivity`, and approximately once per day, check and
see if a newer version of Pachli is available, and prompt the user to
update by going to the relevant install location (Google Play, F-Droid,
or GitHub).
The dialog prompt allows them to ignore this specific version, or
disable all future update notifications. This is also exposed through
the preferences, so the user can adjust it there too.
A different update check method is used for each installation location.
- F-Droid: Use the F-Droid API to query for the newest released version
- GitHub: Use the GitHub API to query for the newest release, and check
the APK filename attached to that release
- Google Play: Use the Play in-app-updates library
(https://developer.android.com/guide/playcore/in-app-updates) to query
for the newest released version
These are kept in different build flavours (source sets), so that e.g.,
the build for the F-Droid store can only query the F-Droid API, the UI
strings are specific to F-Droid, etc. This also ensures that the update
service libraries are specific to that build and do not
"cross-contaminate".
Note that this *does not* update the app, it takes the user to either
the relevant store page (F-Droid, Play) or GitHub release page. The user
must still start the update from that page.
CI configuration is updated to build the different flavours.
The preference change listener was being optimised out by R8, causing
rapid garbage collection, breaking the `changes` flow in release builds.
Fix this by annotating the field with `@Keep` so it is retained.
Fixes#225
Previous code always set `navigationBarColor` and `statusBarColor` to
`transparent` irrespective of the API level.
This only works on API 29 and above; if you do it on API levels lower
than that the system navigation buttons (home, back, recents) are
typically shown on a very similar colour to the background, making them
very hard to see.
Fixes#221
The previous code used `androidx.appcompat.widget.Toolbar` in a several
places.
It's better to use `MaterialToolbar` as that plays better with other
Material components.
Update the usage throughout the project.
In addition, implement a lint check that will prevent any future use
from creeping back in.
Fixes#28
Use Timber instead of `android.util.Log`. Removes the need for `TAG`
statics in companion objects, slightly simplifying the code. Opens the
door for some production logging in the future.
Previously the middle-of-screen progress spinner and the spinner that
appears on a swipe-to-refresh could get out of sync.
Fix this by removing the middle-of-screen progress spinner from relevant
fragments, as the swipe-to-refresh spinner shows the user that an
operation is in progress, and also clues them in to the fact that a
swipe-to-refresh is possible (by using the common UX control).
Fixes#75
There's a well-hidden `updateLintBaseline` task that does what the
custom `newLintBaseline` task does. Prefer the `update...` task to
reduce the amount of custom machinery in this build.
Previously, ending a drag on an image (that didn't result in dismissing
the fragment) animates the image back in to position restoring the X
axis scale factor.
The Y axis scale factor was not restored, potentially breaking the
image's aspect ratio. Restore the Y axis scale factor to fix this
(`ViewVideoFragment` already handles this correctly).
Fixes#202
This previous code could crash if `filterModel.kind` (marked `lateinit`)
had not been set before the filters are loaded. This could happen in
rare cases.
Fix this by rewriting `FilterModel`. Instead of creating a half-empty
object that still needs further initialisation, delay the creation until
all the necessary information is available, and pass it in the
`FilterModel` constructor.
This also forces code that uses `FilterModel` to properly handle the
case where it might be null at the point where filtering decisions have
to be made.
This means that `TimelineViewModel` (and subclasses) no longer need the
`init()` function to complete their construction, which was another
significant code smell. Pass the `TimelineKind` to the view models via
their `SavedStateHandle`.
This showed that changing filters wasn't causing the timelines to update
without a manual refresh, so fix that too. Editing filters sends change
events for the old and new contexts (in case a context is removed from a
filter), and deleting a filter sends a change event too.
Previous code called filters that affect threads "Conversations".
Correct this to "Threads", to distinguish from "Direct Messages" which
can also be known as "conversations" (in the API in particular).
Previous copyright notice mentioned that the license should have been
distributed with Tusky. Correct that to Pachli.
This does not change the copyright assignment, only the instructions
as to where to find the license.
A "self-boost" is someone boosting their own post. Some people are
particularly prolific at this, and it can clutter the timeline. Provide
a new preference that allows the user to show/hide these boosts from
their timeline.
Most parts of the UI will truncate the display of long names (display
and user), but it makes sense to show them untruncated on the account's
profile screen.
The previous code (a) used an emoji as the prefix character when showing
the destination of an obscured link, and (b) made the destination part
of the link anchor text.
Using an emoji was a problem because the user can use different emoji
sets and it can give strange results. Making the destination part of the
link text made it difficult to distinguish at a glance where one link
ends and another starts.
Fix the emoji problem by replacing the emoji with a drawable.
Fix the destination problem by changing the string resource so it only
includes the destination part, and inserting it at the end of the link
instead of replacing the whole link. This means the disclosed
destination is not clickable, does not look like part of the link, and
stands out more.
Previously was showing pale text on a pale background, so effectively
invisible.
Use `colorPrimary` and `colorOnPrimary` to ensure the text can be seen.
Using a sealed interface (instead of a sealed class) at the root of the
hierarchy avoids the overhead of having to create and initialise the
class (visible in the generated bytecode).
It also makes the instantiation code slightly less cumbersome because
the code doesn't need to pass parameters to the root's constructor.
Previously, playing a video would show the controls and associated
overlay for five seconds before fading them out. This obscures the video
for too long.
Fix this by:
- Only showing the media description on start, and remove after two
seconds
- Show the controls (and media description) if the user taps, removing
after two seconds
- Pausing the video (with the pause control, or tapping on the media
description) keeps the controls and description on-screen indefinitely
so they are easier to read
Fixes#144
The previous code didn't include SHOW_CARDS_IN_TIMELINES in the list of
prefkeys that change `StatusDisplayOptions`, so changing the preference
wouldn't update the timeline display; you had to close/restart the app.
Display the time that an announcement was posted, as well as the
most recent update to the announcement (if there is one). Time display
honours the user's "use absolute time" preference.
Fixes#35
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.
Previous code had to distinguish between showing an attachment or
showing an image by URL.
Simplify this by -- in the image URL case -- creating a fake attachment
that references the image URL.
Move the code that unmarshalls the Bundle arguments to
`ViewMediaFragment` to share between `ViewImageFragment` and
`ViewVideoFragment`.
Previous code finalised the view setup in `onViewCreated`, so if you
opened some media, switched away from the app, and switched back you'd
get a blank screen.
Fix this by doing the finalisation in `onResume()`, so the media is
displayed correctly when returning to the fragment.
Fixes#161
Android 14 (SDK 34) requires a `foregroundServiceType` and `onTimeout()`
implementation for foreground services, otherwise creating the service
will crash.
Do this. If `SendStatusService` does timeout then any pending statuses
are marked as failed, saved to drafts, and the user is informed.
Fixes#162
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
Previously the tests mocked shared preferences with a map and a mock
that had to be implemented for each test that needed it.
Replace this with `InMemorySharedPreferences`, which provides the normal
`SharedPreferences` interface so can be used as a drop-in replacement.
Associated changes:
- Handle new null/non-null type signatures in overriden methods
- Configure Robolectric to use SDK 33 (current highest supported
version)
- 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
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`.
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
Start building infrastructure to automatically build and deploy the
`orangeRelease` variant to Google Play.
The variant needs an automatically incrementing `versionCode`. That is
derived from the count of all commits.
Change the separator between the version and the build metadata in the
`versionName` from `-` to `+` to be consistent with semantic versioning.
This is still an experiment, so the workflow is triggered manually and
only uploads to the internal track
The previous code ran the API call in a `try/catch block`, and handled
errors in the `catch`. But `NetworkResult` already catches the exception
and transforms it to a failure, so the error case was not handled.
Replace with `NetworkResult.fold`.
Remove the rxjava3 `Single` type from the MastodonAPI definition,
replacing with `Response` or `NetworkResult` as appropriate.
Update callsites and tests as appropriate.
This removes the need for `com.squareup.retrofit2:adapter-rxjava3`
A previous change dropped the check to see if media was marked as
sensitive, and so all media was hidden when viewing a thread. Reinstate
the check so only sensitive media is hidden (if the user preferences are
set that way).
Previous code created `statusDisplayOptions` in full each time, risking
the creation of inconsistent states / defaults.
Refactor to use `StatusDisplayOptions.from()` so the user's settings
(and defaults) are always respected.
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
Previous code `include`'d `toolbar_basic` inside an `AppBarLayout`.
But `toolbar_basic` already contains an `AppBarLayout`, which
resulted in some rendering issues.
Remove the `include` and incorporate the `MaterialToolbar` directly.
Set the toolbar to scroll out of the way when the screen scrolls, so
the behaviour is consistent with the tabs in `MainActivity` and
`AccountActivity`.
Previously, in `MainActivity` and `AccountActivity` the status bar would
be `colorPrimaryDark`.
Adjust the layouts and code so that `colorSurface` is used to match
the toolbar colour.
Fixes#79
The previous code generally converted between a higher and a lower type
by putting the type conversion functions on the lower type.
This introduced cycles in the code dependency graph, and made it more
difficult to follow the code flow.
Refactor the code so that types generally have a `from(...)` static
factory method that can create an instance from a lower type, and if
appropriate a `to...()` method that can also create an instance of that
lower type.
Add `docs/code-style.md` which explains the rationale for this change
in more detail so that future contributors can write code in the same
style.
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).
Use `assert` to note when a nullable value is known to be non-null.
Extract a method call to a variable where necessary to do this.
Update `CharSequence.unicodeWrap()` to handle a null `CharSequence`.
`ConversationViewHolder` calls `getDisplayName()`, which may return
null.
Replace with `getName()`, which is consistent with usage in other
classes. Mark `getDisplayName()` as deprecated to prevent future
usage.