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.
The previous code used SwitchPreference to generate the switches, which
didn't apply the Material colours. This made it difficult to distinguish
between the on/off states, as the non-Material colours for those states
are very similar.
Fix by using SwitchPreferenceCompat which uses the correct Material
colours.
The previous code used "?attr/colorOnTertiary", which is the wrong
colour for the default background. Remove the override, so the correct
styled colour is used.
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.
Testing showed additional activities with toolbar flicking issues. Fix
as before, using `setLiftOnScrollTargetView` to specify the scrolling
view the toolbar should lift above.
The chips for adding a new hashtag to a tab specified the background
colour without setting the text colour, resulting in the colour being
too low-contrast against the background.
Use `?colorOnPrimary` to get the correct colour.
Switching to the Material 3 themes caused the previous list dividers to
disappear.
Replace `DividerItemDecoration` with `MaterialDividerItemDecoration` to
restore them.
The previous code would always fetch the latest statuses when the app
restarts, jumping the user to the top of the home timeline. This is
because state.anchorPosition was null in this case.
Fix this by passing the saved initialKey to CachedTimelineRemoteMediator
and using it to construct a page of statuses around the requested
status.
This restores the user's reading position, and ensures that if the
user is at the top of the list their reading position is not reset
to the second item in the list.
Fixes#41, #42
The previous code did not handle refreshing correctly; it retained some
of the cache, and tried merge new statuses in to the cache. This does
not work, and resulted in the app creating gaps in the timeline if more
than a page's worth of statuses had appeared since the user last
refreshed (e.g., overnight).
Fix this by treating the on-device cache as disposable, as the Paging3
library intends. On refresh the cached timeline is emptied and replaced
with a fresh page.
This causes a problem for state that is not stored on the server but is
part of a status' viewdata (has the user toggled viewing a piece of
media, expanded a CW, etc).
The previous code tried to work around that by pulling the state out of
the page cache and copying it in to the new statuses. That won't work
when the page cache is being destroyed.
So do it properly -- store the viewdata state in a separate (sparse)
table that contains only rows for statuses that have a non-default
state.
Save changes to the state when the user interacts with a status, and
use the state to ensure that the viewdata for a status in a thread
matches the viewdata for the same status if it is shown on the home
timeline (and vice-versa).
Fixes#16
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
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
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
The previous code was operating on the wrong text, resulting in normal
URL spans (which have an underline) being applied, instead of the
correct spans (which don't).
Fix this by using the correct length.
Previous code created hashtag filters without the `#`, so muting the
tag `#something` would filter all posts that contained `something`,
with or without the `#`.
Fix this when creating filters, and only remove filters (when unmuting)
if the title and contents match.
Show a snackbar when hashtags are successfully muted/unmuted, so the
user is aware something happened.
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.
- 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
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
Filters that the user had set for the notifications timeline were not
being applied.
Fix this in NotificationsViewModel; fetch the user's filters and apply
them against the status in a notification. If the status should be
hidden it is removed, and if it should show a warning it does so.
The user can click through the warning to show the status.
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
Android's choices for font customisation can be limited, depending on
the vendor. Allow users to choose from a small collection of embedded
fonts, chosen by asking users for recommendations.
The font choice is implemented as a preference. Provide a custom dialog
that shows the fonts (in that font) so the user can see what they're
choosing between.
Ensure the font's license information is displayed in the "About"
section.
The previous code did not credit all third party code used in the app,
or provide access to the licenses.
Fix this by adopting the "aboutlibraries" library, which processes
dependencies at build time and generates a list of dependencies,
versions, and license information to display to the user.
Use this to also ensure that the non-source dependencies (artwork,
emoji) are given appropriate credit.
- Remove the existing restriction on the number of tabs
- Allow the tabs to scroll to display more
- Update UI and text resources to remove obsolete content
- Implement the trending posts API
- Display trending statuses as a new Timeline kind
- Allow the user to add trending statuses to a dedicated tab
- Always show the "Trending" option in the navigation menu
- Implement the trending links API
- Provide a Fragment/ViewModel/Repository and Adapter/ViewHolder set
to display the content
- Show all trends (as a pageable fragment list) in TrendingActivity
- Allow the user to add trending links to a dedicated tab
- Always show the "Trending" option in the navigation menu
### Objective
* Prevent data loss when the user inadvertently hits back or wants to
leave the profile edition with unsaved changes.
### Description
* To limit the number of changes to the existing codebase, I merely
re-used the same method used by `save()` in the ViewModel to decide
whether to make a network request or simply return the profile as-is.
* ~A bit of code juggling around in the ViewModel and I was able to use
the logic for all the encoding of each profile field (Which is what the
ViewModel caches in memory).~ Thanks @Lakoja for improving this in the
VM.
* A couple of internal data classes used as helpers to move all the
fields around (now that they are no longer used in one single place)
were introduced.
### Potential Optimizations
* ~The profile encoding is done twice (once for checking, and then again
if the user has to actually save it). I'd say this is a negligible price
to pay, since the alternative would be to create a different set of
comparisons and/or keeping another profile in memory for the purpose of
comparison.~
### Visual Improvement
* I believe the Dialog is difficult to see, but it's being displayed
with Tusky's theme. Perhaps there's a better style to apply in this
case? (or maybe the edit profile activity shouldn't have the same
background color as dialogs?!)
### Issue
* #3486
Set the "System Design" as the default theme.
This ensures that the app's initial behaviour respect's the user's system-wide theme choice, while still allowing the user to adjust it later.
This is only done for new installs of Tusky. If the user is upgrading from a previous release and they did not have an explicit theme set then the dark theme is used, and the UX does not change.
dc9e9f2aeb
modifed the code that fetched the value of EXTRA_NOTIFICATION_TYPE in an
intent, to use getSerializable().
However, the value was being placed in to the intent using putString().
This caused an exception when trying to update the summary notification,
so it would never update.
```
java.lang.ClassCastException: java.lang.String cannot be cast to com.keylesspalace.tusky.entity.Notification$Type
at com.keylesspalace.tusky.components.notifications.NotificationHelper.updateSummaryNotifications(NotificationHelper.java:321)
at com.keylesspalace.tusky.components.notifications.NotificationFetcher.fetchAndShow(NotificationFetcher.kt:87)
at com.keylesspalace.tusky.components.notifications.NotificationFetcher$fetchAndShow$1.invokeSuspend(Unknown Source:14)
```
Fix this by placing the value in to the intent using putSerializable(),
to match how it will be fetched.
Previously the notification filter and clear actions were shown as
buttons in the UI, with a preference that determined whether they were
displayed.
Remove this preference, and display them as menu items.
- "Filter notifications" is shown as an icon, if possible
- "Clear notifications" is only ever shown as a menu item, to reduce the
chance the user inadvertently selects it
To ensure that the options menu appears correctly, remove the code that
creates a "fake" action bar, and adjust the layouts so that there are
three toolbars;
- mainToolbar -- displays the icons, and the current "location" (Home,
Notifications, etc)
- topNav -- displays the row of tabs at the top
- bottomNav -- displays the row of tabs at the bottom
Only one of them is set as the support action bar (depending on the
user's preferences). This provides the "show a logo" and "show the
options menu" functionality as standard, without needing to re-implement
as the previous code did.
The "trending" functionality will expand to include trending links and
posts. But at the moment the "Trending" references in the code are
exclusively to hashtags.
Rename "Trending" to "TrendingTags" or similar everywhere necessary in
order to prepare for this.
This includes a database migration, as the identifier for the "Trending
tags" tab in the account preferences was changed from "Trending" to
"TrendingTags". The migration updates the stored value if necessary.
Before, intent creation was scattered across multiple sites, with account switching logic in both `ComposeActivity` and `MainActivity`.
Now, intents are only created in `MainActivity` Companion, and account switching only occurs in `MainActivity`
Fixes#3695
Prevent users from accidentally deleting filters by prompting them to confirm.
Add an AlertDialog extension that converts AlertDialog callbacks to linear control flow.
Fixes#3736.
Currently translated at 100.0% (617 of 617 strings)
Translated using Weblate (Vietnamese)
Currently translated at 100.0% (617 of 617 strings)
Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
Previously, the thread indicator would start at the top of the avatar
for the status at the start of the thread, and end at the top of the
avatar for the status at the end of the thread.
If these avatars were partially transparent the thread indicator could
either (a) poke out of the top of the avatar at the start of the thread,
(b) not properly connect with the avatar at the end of the thread, or
(c) both.
Partially fix this by making the divider start/stop in the middle of the
avatar. This assumes that this area will typically have opaque content,
even if some of the rest of the avatar is transparent. This is not
always true, but it's still better than the current behaviour.
Avatars that are semi-transparent are a problem when viewing a thread,
as the line that connects different statuses in the same thread is drawn
underneath the avatar and is visible.
Fix this with a CompositeWithOpaqueBackground Glide transformation that:
1. Extracts the alpha channel from the avatar image
2. Converts the alpha to a 1bpp mask
3. Draws that mask on a new bitmap, with the appropriate background
colour
4. Draws the original bitmap on top of that
So any partially transparent areas of the original image are drawn over
a solid background colour, so anything drawn under them will not appear.
If:
1. You're viewing an account's media tab
2. Some of the media was marked sensitivei
3. The `alwaysShowSensitiveMedia` setting was `true`
tapping on the image (once) would do nothing visible, because it was
treated as the "reveal sensitive media" tap. You had to tap on it a
second time to open it.
Fix this, by passing the preference value through to the relevant code.
---------
Co-authored-by: Tiga! <maxiinne@proton.me>
To determine the earliest day to show in the calendar, take the current
date/time, add the minimum scheduled seconds buffer (which may roll the
date/time over to the next day), and then clamp to the start of that
day. So it's either today (if the current time + minimum scheduled
seconds is less than midnight) or it's tomorrow.
When displaying the calendar work around a misfeature in Material Date
Picker. It accepts UTC seconds-since-epoch, but does not convert it to
the local time for display.
While I'm here, show the selected day in the time picker's title.
Fixes https://github.com/tuskyapp/Tusky/issues/3916
The "edit" icon when showing a scheduled status' time was grey, so it's
not obvious that this section is clickable.
Use colorPrimary, so it looks more like a button.
The previous code used `notificationTabPosition`, which was never
changed, so always 0.
This meant that if you e.g., got to `MainActivity` by clicking on a
notification, and the notification tab was current, the title would
still show "Home".
Fix that by using the existing `position` variable which represents the
currently selected tab, and ensure the correct title is shown.
Fixes#3864.
Make it easier for people to find information we need for a bug report,
and show it on AboutActivity.
New info is:
- Device manufacturer (e.g., "Google") and model (e.g., "Pixel 4a (5G)")
- Android version (e.g., "13")
- SDK version (e.g., "33")
- Active account (e.g., "@Tusky@mastodon.social")
- Server's version (e.g., "4.1.2+nightly-20230627")
All info is copyable to make it easy to include in a bug report. A
button to copy the information is also shown.
Update to Kotlin 1.9.0 and migrate to newer language idioms.
- Remove unnecessary @OptIn for features migrated to mainstream
- Use `data object` where appropriate
- Use new enum `entries` property
Migrate to touchimageview from photoview, and adjust the touch logic to correctly handle single finger drag, two finger pinch/stretch, flings, taps, and swipes.
As before, the features are:
- Single tap, show/hide controls and media description
- Double tap, zoom in/out
- Single finger drag up/down, scale/translate image, dismiss if scrolled too far
- Single finger drag left/right
- When not zoomed, swipe to next image if multiple images present
- When zoomed, scroll to edge of image, then to next image if multiple images present
- Two finger pinch/zoom, zoom in/out on the image
Behaviour differences to previous code
1. Bug fix: The image can't get "stuck" when zoomed, and impossible to scroll
2. Bug fix: Pinching is not mis-interpreted as a fling, closing the image
3. Bug fix: The zoom state of images is not lost or misinterpreted when the user swipes through multiple images
4. Bug fix: Double-tap zooms all the way, instead of stopping
5. Tapping outside the image does not dismiss it, controls and description show/hide
Fixes https://github.com/tuskyapp/Tusky/issues/3562, https://github.com/tuskyapp/Tusky/issues/2297
Android lint was erroneously warning that the forEach construct in
Kotlin required API 24+, which is incorrect, see
https://issuetracker.google.com/issues/185418482.
Work around that by forcing the Android lint version to 8.1.0.
This triggered some additional checks, which have been ignored, and a
new baseline.
Preferences are shown using view holders.
The previous code did not clear the listeners or hide the icons if
necessary.
The practical upshot of this was that if you had two or more slider
preferences, *and* they were situated more than a screen's height apart,
the viewholder from the first one would get reused.
And if the first one enabled icons then the second one would show them.
And clicking on the second one would also call the listeners for the
first one.
As tests are run against locale JVM and test does not force
a locale to run, so some tests may fail due to a different result only
due to the locale of the JVM used.
Example here with test `same year formatting` in class
`AbsoluteTimeFormatterTest` line 30 on a French JVM. There may be other
lines to fail with other languages.
Fixes#3859
Currently translated at 100.0% (609 of 609 strings)
Translated using Weblate (Vietnamese)
Currently translated at 100.0% (609 of 609 strings)
Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
Currently translated at 100.0% (609 of 609 strings)
Translated using Weblate (Vietnamese)
Currently translated at 100.0% (604 of 604 strings)
Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
Currently translated at 100.0% (609 of 609 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (604 of 604 strings)
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/
Translation: Tusky/Tusky
Currently translated at 100.0% (609 of 609 strings)
Translated using Weblate (Persian)
Currently translated at 100.0% (609 of 609 strings)
Translated using Weblate (Persian)
Currently translated at 100.0% (604 of 604 strings)
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/
Translation: Tusky/Tusky
GBoard and other IME's support pasting images, which are converted to attachments.
Sometimes these have labels that describe the image. If present, set it as the default alt-text.
Fixes#3799
Fix a crash where workers, in some conditions, should show a notification. These are sent to a dedicated channel with no importance.
Convert NotificationWorker to a CoroutineWorker and remove its use of `runBlocking`.
Fixes#3754
Most lists in the app use (explicitly or implicitly) platform metrics for dimensions, text size, colour, and so on, possibly via styles.
A few don't, inadvertently using the user's setting for status text size
Fix these, and simplify code where possible.
- Use android attributes for padding and height, for consistent UX.
- Remove explicit usage of app:tabTextAppearance, rely on the style.
- Remove ListSelectionAdapter and item_picker_list.xml, and adjust TabPreferenceActivity to use an ArrayAdapter with simple_list_item_1.xml
- Simplify item_followed_hashtag.xml, consistent with item_list.xml.
Fixes https://github.com/tuskyapp/Tusky/issues/3131
Font scaling is applied in addition to any scaling set in Android system preferences. So if the user set the Android font size to largest (a 1.3x increase) and then sets the preference to 120%, the total change is 1.56x.
Create SliderPreference to adjust the preference.
- Use Slider, which supports float values and step sizes > 1
- Display the selected value in the preference's summary
- Provide buttons to increment / decrement the value
Restart the activity if the preference changes so that the user sees the impact of the change immediately. Fix a bug in PreferencesActivity where the "EXTRA_RESTART_ON_BACK" intent was never processed. Fix this to ensure that other activities are restarted so the new font scale takes effect.
Implement the scaling in BaseActivity by overriding onAttachBaseContext, and providing a wrapped context with the font scaling applied.
Fixes https://github.com/tuskyapp/Tusky/issues/2982, https://github.com/tuskyapp/Tusky/issues/2461
Instead of repeating the same if/else check on the error type when setting up the background message, move this in to BackgroundMessageView.
Provide different `setup()` variants, including one that just takes a throwable and a handler, and figures out the correct drawables and error message.
Update and simplify call sites.
Currently translated at 100.0% (604 of 604 strings)
Translated using Weblate (Persian)
Currently translated at 100.0% (603 of 603 strings)
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/
Translation: Tusky/Tusky
This can happen if the edit history has not been propogated to the user's server.
If the edit history is missing then show an error with a link to the specifc Mastodon issue.
Fixes#3743
When the user is closing the compose view,
if it's new and empty, don't show a prompt.
if it's an existing draft and now empty, ask if the user wants to delete it or continue editing. I don't think there is much value in saving an empty draft.
---------
When NotificationWorker was moved from ...components.notifications to
...worker.
Installing Tusky with this change doesn't remove any future periodic
jobs queued under the old class name. So when Class.forName() is
called the old class name is not found, and the exception is thrown.
Handle this the same way androidx.work.WorkerFactory does -- catch
the exception, log it, and return null.
Fixes#3740
- Create a flow with new items (arbitrary ints) when a reload from the top should happen
- Combine this flow with notificationFilter, so changes to either of them trigger a reload
- Provide a menu item in NotificationsFragment to initiate the reload
- Handle the action in the view model
Remove the use of ReplacementSpan. It turns out this span type is incompatible with spans that occupy more than one line, and the result is that a longer diff can run off the end of the screen. The alternative means that the diff'd text doesn't have additional padding and rounded corners, but it's better than not being visible.
Display the most recent version of the status with larger text. Again, consistent with the thread view.
Display the avatar, name, and username of the poster in a pinned header at the top of the screen, instead of duplicating the information on every edit. This reduces the amount of redundant information on the screen.
The Android libraries have a bug where a TextView can forget that it contains selectable text, can be pasted in to, etc.
See https://issuetracker.google.com/issues/37095917
Fix this with an extension method that toggles the selectable state to re-enable it, and use this on the profile fields when editing an account.
Fixes https://github.com/tuskyapp/Tusky/issues/3706
- Use NO_POSITION instead of hardcoding 0.
- Don't set a state restoration policy, PagingDataAdapter already does that
- Return the closest item, not just the closest page, in getRefreshKey
It caused text size differences between the text in this view and all the other textviews in this layout.
It's not used in other layouts.
Fixes https://github.com/tuskyapp/Tusky/issues/3494
This will make tests that need it easier.
- Rename from AccountPreferenceHandler
- Inject its dependencies
- Create an injectable CoroutineScope it can use for launching coroutines
- Use it in AccountPreferences
Currently translated at 100.0% (601 of 601 strings)
Translated using Weblate (Persian)
Currently translated at 100.0% (601 of 601 strings)
Translated using Weblate (Icelandic)
Currently translated at 100.0% (601 of 601 strings)
Co-authored-by: Nik Clayton <nik@ngo.org.uk>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
Currently translated at 100.0% (601 of 601 strings)
Translated using Weblate (Icelandic)
Currently translated at 99.8% (600 of 601 strings)
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
In the previous code any errors that occured *before* a subscriber was
listening to `uiError` would be dropped, so the user would be unware
of them.
By implementing as a channel these errors will be shown to the user,
with an opportunity to retry the operation or report the error.
Introduce Flow<T>.throttleFirst(). In a flow this emits the first value,
and each value afterwards that is > some timeout after the previous
value.
This prevents accidental double-taps on UI elements from generating
multiple-actions.
The previous code used debounce(). That has a similar effect, but with
debounce() the code has to wait until after the timeout period has
elapsed before it can process the action, leading to an unnecessary
UI delay.
With throttleFirst a value is emitted immediately, there's no need
to wait. It's subsequent values that are potentially throttled.
- Extend what was `NotificationWorkerFactory` to `WorkerFactory`. This
can construct arbitrary Workers as long as they provide their own
Factory for construction.
The per-Worker factory contains any injected components just for that
worker type, keeping `WorkerFactory` clean.
- Move `NotificationWorkerFactory` to the new model.
- Implement `PruneCacheWorker`, and remove the code from
`CachedTimelineViewModel`.
- Create the periodic worker in `TuskyApplication`, ensuring that the
database is only pruned when the device is idle.
formatNumber() was existing code to show numbers with suffixes like K, M, etc, so re-use that code and delete shortNumber().
Update the tests to (a) test formatNumber(), and (b) be parameterised.
* Fetch all outstanding Mastodon notifications when creating Android notifications
Previous code fetched the oldest page of unfetched Mastodon notifications.
If you had more than a page of Mastodon notifications you'd get Android notifications for that page, then ~ 15 minutes later Android notifications for the next page, and so on.
This code fetches all the outstanding notifications at once.
If this results in more than 40 total notifications the list is still trimmed so that a maximum of 40 Android notifications is displayed.
Fixes https://github.com/tuskyapp/Tusky/issues/3648
* Build the list using buildList
Don't use `accountManager.activeAccount` throughout the code.
Instead, get the active account once, and use that over the life of the viewmodel.
As shown in https://github.com/tuskyapp/Tusky/issues/3689#issuecomment-1567219338 the active account can change before the view model is destroyed, and if that happens account information for the account will be written to the wrong account.
Fix a bug where the active account can be overwritten.
1. Have two accounts logged in to Tusky, A and B
2. Open Tusky as account A
3. Send a DM to account B (doesn't have to be a DM, just anything that creates a notification for account B)
4. Restart Tusky so the Android notification for the DM is displayed immediately. You are still acting as account A.
5. Drag down the Android notification, you should see two options, "Quick reply" and "Compose"
6. Tap "Compose"
7. ComposeActivity will start. You are now acting as account B. Compose and send a reply
8. You'll be returned to the "Home" tab.
The UI will show you are still account A (i.e., it's account A's avatar at the top of the screen, if you have the "Show username in toolbars" option turned on it will be account A's username in the toolbar).
But you are now seeing the home timeline for account B.
Fix this.
ComposeViewModel
- Do not rely on the active account in sendStatus(), receive the account ID as a parameter
ComposeActivity
- Use either the account ID from the intent, or the current active account. **Do not** change the active account
- Pass the account ID to use in the sendStatus() call
The previous code gets the user's reading position *once*, when the
viewmodel is created, and uses that whenever it needs to be restored.
This is a problem. Suppose the user is a few days behind on their
notifications, and opens the app.
The reading position is restored, as expected. They scroll up to start
reading newer notifications.
Then they change their notification filters. This causes the
notifications list to change, and when it does their reading position
is set back to what it was when they first switched to the Notifications
tab.
Fix this by:
NotificationsFragment:
- Save the reading position whenever the user stops scrolling.
NotificationsViewModel:
- Use the saved reading position whenever the list of notifications
can change, not just when the view model is created.
Not all servers support the marker API. If they don't the user will
potentially get duplicate Android notifications.
To resolve this, store a copy of the notification marker ID locally as
well. Defer to the remote marker if it exists (and is newer than the
local marker).
Fixes https://github.com/tuskyapp/Tusky/issues/3671
When fetching:
- Maintain a marker with the position of the newest fetched notification
- Use the marker to determine which notifications to fetch
- Fetch notifications with min_id to ensure that none are lost
- Update the marker as necessary
- Perform a one-time immediate fetch of notifications on startup
When creating notifications:
- Identify each notification with tag=${MastodonNotificationId}, id=${account.id}
- Remove activeNotifications field, it's no longer necessary
- Use the tag/id tuple to reliably identify existing notifications and avoid creating duplicates
- Cancelling notifications for an account must iterate over all the notifications, and individually remove the notifications that exist for that account.
- Limit notifications to a maximum of 40 (excluding summary notifications)
- Remove notifications (oldest first) to get under this limit
- Rate limit notification creation to 1 per second, so the OS won't drop them
Adjust the summary notification:
- Ensure the summary notification and the child notifications have the same group key
- Dismiss the summary notification if there is only one child notification
NotificationClearBroadcastReceiver is no longer needed, so remove it, and the need for deletePendingIntent.
Fixes#3625, #3539
Currently translated at 98.0% (589 of 601 strings)
Translated using Weblate (Belarusian)
Currently translated at 95.0% (571 of 601 strings)
Co-authored-by: xzFantom <xzfantom@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/be/
Translation: Tusky/Tusky
Currently translated at 100.0% (601 of 601 strings)
Translated using Weblate (French)
Currently translated at 97.8% (588 of 601 strings)
Translated using Weblate (French)
Currently translated at 89.5% (538 of 601 strings)
Translated using Weblate (French)
Currently translated at 79.5% (478 of 601 strings)
Co-authored-by: codl <codl@codl.fr>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/
Translation: Tusky/Tusky
- Add a field to AccountEntity to hold the reading position
- Provide a method to save in the viewmodel to save the position
- Save the position when TimelineFragment pauses
Does not restore the position yet.
Requesting a non-existent notification ID will cause the fall-back requests to succeed but return empty lists which stops pagination.
Check for this, and in the worst case, fall back to returning the most recent notifications.
On Account preferences > Filters
With the list hidden on an empty view the FAB is erroneouly placed on the top left of the screen.
(Introduced with #3430)
Add additional 6dp margin to the top of the username view, to provide more space after the "X <performed an action>" view, consistent with item_follow.xml.
The pageSize is large enough that there's no need for the default 3 x pageSize initialLoadSize value, and this improves performance.
Fixes https://github.com/tuskyapp/Tusky/issues/3578
This makes the notification view for a follow request contain more info about the new follower, and makes the layout (of their name / username) consistent with other notifications that show names/usernames.
Previous code would discard the image alt-text if the user finished writing the text before the image had finished uploading.
This code ensures the text is set after the image has completed uploading.
* Disable lint checks for unused resource IDs
The check doesn't catch some instances where resources are used through viewbinding, and has too many false positives to be useful.
* Regenerate lint baseline
Delete the existing file, then regenerated with `.\gradlew lintBlueDebug -Dlint.baselines.continue=true`
* 3503: SwipeRefreshLayout must be higher level
* 3503: Fix notifications view a bit
* 3503: Wrap recycler views and message views in the swipe-to-refresh if all are present
* 3416: Call the list activity when empty; show failure on loading
* 3416: Revert include grouping
* 3416: Remove faulty include after merge
* 3416: Added a list loading progress
* 3416: Add proper padding to progress
* 3416: Show a text if there are no lists, do not change dialog title
* 3416: Center things in layout
* 3416: Appease linter (?)
* 3416: Do not hide manage lists button
* 3416: Use ThemeUtils
Currently translated at 100.0% (600 of 600 strings)
Translated using Weblate (Galician)
Currently translated at 94.0% (564 of 600 strings)
Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
Currently translated at 95.5% (573 of 600 strings)
Translated using Weblate (Spanish)
Currently translated at 95.1% (571 of 600 strings)
Co-authored-by: Paul Sanz <registro@polkillas.net>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/es/
Translation: Tusky/Tusky