`NotificationDao.pagingSource` missed the `reportId` column and
mis-soelled `report_ruleIds`, risking an NPE.
Fix that.
While I'm here, add suggested indices to `NotificationEntity`.
Previous code showed the post being replied to, but in a tiny font and
hidden behind a disclosure triangle that was difficult to spot.
Fix that. Show the post, including the author and their avatar. The text
of the post is selectable for copy/paste in to the reply if necessary
(links and hashtags are deliberately not clickable so they can't be
clicked by accident).
To do this:
Update `ComposeOptions`. Instead of three different properties that
were set if the post was a reply, use a new `InReplyTo` sealed class
that covers two situations; either the caller has the full content of
the status being replied to, in which case it's included, or they only
have the ID of the status being replied to.
Update `ComposeViewModel` with a state flow of `inReplyTo` results to
represent either (a) Not a reply, (b) is a reply, data is loading, (c)
is a reply, data is loaded, or (d) is a reply, error occurred loading
data.
In `ComposeActivity` use this flow to drive updates to a new part of
the UI showing the status being replied to (or hiding that part of the
UI if this is not a reply).
Previous code used five (!) different types for the network response.
Some used Retrofit's `Response`. This provides access to the headers.
Some used `NetworkResult`. This did not provide access to the headers,
but did provide some higher-order functions (e.g., `fold`) for operating
on the results.
One used a raw `Map`.
One used a raw `Call`.
The rest had been converted to `ApiResult`, a `Result<V, ApiError>`.
This provides the higher-order functions, provides the headers, and
is exception-free, so is the correct type to use.
This PR completes the work of cutting over to `ApiResult`. The return
values are changed and the calling code is adjusted to use the new
functions as appropriate.
Previous code complicated the logic to retrieve and fetch a jumbo page
of results around a given status/notification ID. Simplify it to make it
easier to follow.
# Status storage
Re-work how statuses are stored and managed to separate the cached home
timeline from the cached notification timeline.
Previously, the home timeline pulled all statuses in `StatusEntity`.
Since that table also includes statuses that are referenced from
`NotificationEntity` this could show the wrong data. It also makes it
difficult to cache other timelines in the future.
To fix this:
- Introduce `TimelineStatus` which associates a given timeline with the
statuses on that timeline.
- Use the the `StatusEntity` table as a general cache of statuses.
wherever they're used.
- Create the home timeline by joining `TimelineStatus` (where the
timeline's kind is `Home`) with the statuses in `StatusEntity`.
This has a number of knock-on effects.
- Deleting from the home timeline now deletes the association from
`TimelineStatus`. The cached status is unaffected, so if it is
referenced from another cached timeline (currently, notifications)
there is no change.
- Modifying a status on one timeline (translating, expanding,
collapsing, etc) modifies it on all timelines that reference that
status.
- `cleanup()` and related functions no longer need to take `limit` or
`keep` parameter, as it's known whether a status is referenced from a
timeline.
Rewriting one of the queries exposed an issue where `TimelineDaoTest`
run locally could return different (incorrect) results to the same test
run on a device (https://issuetracker.google.com/issues/393685887).
So re-implement `TimelineDaoTest` as an `androidTest`, and update the CI
workflow to include a step to run these tests on an API 31 emulator.
# Repositories
- Allow `null` as an initial key.
# Fragments
- Remove unnecessary `refreshAdapterAndScrollToVisibleId`.
Previous code used a transaction for updates to the database, but didn't
do the earlier reads in the same transaction. Theoretically this could
race.
Guard against this by using a single transaction for a complete remote
mediator operation (refresh, prepend, append).
TimelineDao contained operations on timelines and statuses which made it
large and confusing.
Factor out the status-specific operations in to the new StatusDao class
to make things more understandable.
TimelineStatusEntity is going to be repurposed for the table that maps
between a timeline (not just the home timeline) and the statuses on that
timeline.
These are cached timelines, backed by Room. Room **requires** the
`PagingConfig` to have `enablePlaceholders = true`. Otherwise the list
is corrupted when scrolling down the list and paging in new items.
To restore the user's reading position correctly in the UI, wait for the
adapter to emit the very first page. Combine this with the user's
refresh key, and the number of placeholders in the page, to scroll the
user to the correct place in the list.
To make all this work, ensure that Room loads a large enough page of
data around the refresh key (in the `initialKey` calculation).
The database queries in the @Query annotations were in a range of
different styles which made them difficult to read, and difficult to
write new ones in a consistent style.
Fix this.
Write a new tool, `sqlfmt`. This processes the DAO files looking for
`@Query(...)` annotations. It extracts the SQL from those annotations
and calls `sqlfluff` (https://github.com/sqlfluff/sqlfluff, which must
be installed separately) to lint and fix formatting issues in the SQL.
The file is re-written with the newly formatted SQL queries.
Previous queries to delete stale data from the database could fail due
to the new foreign key constraints.
Rewrite them so statuses and accounts referenced by cached notifications
are not deleted.
Previous code had legacy `try ... catch` blocks that could catch all
exceptions, including `CancellationException`, thrown if the job of a
suspending function is cancelled.
Indiscriminately catching those can interfere with cancellation, so use
`currentCoroutineContext().ensureActive()` to rethrow the exception if
the job has been cancelled.
When constructing an ApiError the *original* request is used, because
Retrofit cannot access the request after its been through OkHttp
interceptors. So Retrofit cannot know the actual domaint the request was
sent to, and the "dummy.placeholder" domain is displayed.
To prevent user confusion, and since this can't be corrected, display
just the path and query part of the URL in the error message. This is
still sufficient to diagnose the precise API call and parameters that
resulted in the error.
Fixes#1217
The default migration code copies existing NotificationEntity rows to
the new table. This might fail because of the new FK constraint on
TimelineAccountEntity. Fix this by not copying the data over; it's a
local cache, so nothing of importance is lost.
The modifications to the Notifications* classes highlighted different
(and better) ways of writing the code that manages status timelines.
Follow those practices here.
Changes include:
- Move `pachliAccountId` in to `IStatusViewData` so the adapter does not
need to be initialised with the information. This allows the parameter
to be removed from functions that operate on `IStatusViewData`, and the
adapter does not need to be marked `lateinit`.
- Convert Fragment/ViewModel communication to use the `uiResult`
pattern instead of separate `uiSuccess` and `uiError`.
- Show a `LinearProgressIndicator` when refreshing the list.
- Restore the reading position more smoothly by responding when the
first page of results is loaded.
- Save the reading position to `RemoteKeyEntity` instead of a dedicated
property in `AccountEntity`.
- Fixed queries for returning the row number of a notification or
status in the database.
Fixes#238, #872, #928, #1190
Previous code could crash because of a foreign key constraint between
`TimelineStatusEntity` and `TimelineAccountEntity`.
Specifically, `removeAllAccounts` would remove accounts that were still
referenced by cached notifications or statuses (the statuses were
retained because they were referenced by notifications).
Fix this by only removing accounts that are not referenced by anything.
While I'm here, `NotificationEntity` should have an FK constraint to
`TimelineAccountEntity`, so add that.
Persist the user's notification filtering decisions (i.e., the decision
to show a filtered notification) by caching all notification data,
including the filtering decision, in the database.
## Structure changes
This means re-writing the notification management system to use Room and
the Paging library to manage the notification data.
Implement a repository and remote mediator for notifications that does
this, with knock on effects for the viewmodel and the fragment. Take the
opportunity to rewrite these to reflect (current understanding of) best
practice for state management.
Active account information is included in the viewdata for each
notification when sent to the adapter. This allows the adapter to be
created before the fragment knows the active account from the view
model.
`RemoteKeyDao` is extended to support sorting the "refresh" key for
a timeline. This is used to persist the notifications refresh key
instead of the `lastNotificationId` property in the account (which has
been removed).
## UX changes
A linear progress bar is used to show progress when notifications are
refreshed, as part of the ongoing effort to migrate the UI.
Some parts of the UI already showed lists sorted by title, but not all.
The areas fixed are:
- The list of lists in the main drawer (left side navitation)
- The list of lists when adding/removing an account from a list
Fixes#1168
Previous code managed account deletions by having specific functions to
call when an account was deleted that would delete all related data.
Replace this with proper foreign key references back to the account ID,
and cascade account deletes to the related data.
Add tests to ensure the deletes happen as expected. Update existing
tests to create an account where necessary so the new foreign key
constraints are kept.
Previous code inadvertently crashed when the user clicked on a trending
link count to see statuses about the link.
Don't do that. Instead, show the statuses that mention the link, and
show the link's title in the actionbar to make it more explicit for the
user.
Special-case this timeline type in TimelineActivity so it can't be added
to a tab (it would be difficult to distinguish it amongst tabs as they
would have the same icon).
Append the request method ("GET", etc) and the request URL to error
messages in ApiResult errors. This should provide additional inforamtion
when debugging issues reported by users.
Mastodon 4.3 introduced a new API to fetch a timeline of posts that
mention a trending link.
Use that to display a "See <n> posts about ths link" message in a
trending link's preview card (if supported by the server).
Define a new timeline type with associated API call to fetch the
timeline.
Add an accessibilty action to support this.
While I'm here also support author's in preview cards that don't have a
related Fediverse account; show their name in this case.
Fixes#1123
A few places in the code were calling `moshi.adapter` to marshall
to/from strings in the database where type converters either already
exist, or are straightforward to create.
Create the missing type converters, and use them throughout. This
simplifies several places where a Moshi instance no longer needs to be
passed through several layers of method calls.
Since this doesn't change the underlying database representation of the
data there's no need to bump the database version number.
Allow the user to define filtering rules for notifications by sending
account:
- Not followed
- Younger than 30d
- Limited by moderators
and a policy for each of either show, warn, or hide.
To do this:
## Manage followers
- Create a new `FollowingAccountEntity`, to record accounts the logged
in account is following.
- Fetch the account's followers when an account is made active, and
persist to this table.
- Provide the followers as a property on `PachliAccount`
- Update this table if the user follows/unfollows accounts during normal
operation.
## Track account creation time
- Record account creation time in `TimelineAccount`.
## Track notification creation time
- Record notification creation time in `Notification`.
## API
- Always fetch all notifications, including those the server is
filtering.
## UX and storage for account filters
- Show a new Account preference to edit account notification filters.
- Display a dialog to manage account notification filters.
- Persist the user's choice to new properties in `AccountEntity`.
- New `AccountManager` methods to update the properties
## Filtering notifications
- New `NotificationFilter.filterNotificationByAccount()` method to make
the filtering decision based on the user's preferences.
- Use this in `NotificationFetcher` to filter notifications before
creating Android notifications.
- Use this in `NotificationsViewModel` to filter notifications before
display in `NotificationsFragment`.
## UX for filtered notifications
- Display filtered (with warning) notifications inline with other
notifications, with UI to disclose the notification or edit the filters.
This code will be used elsewhere in an upcoming change, so extract it
now to minimise the diffs.
While I'm here, provide an icon for mentions, and an attribute for the
"favourite" colour.
Pleroma (and possibly other servers) can return dates that have no
timezone. Previous code would fail to deserialise JSON in this state and
show an error.
Patch around this by assuming anything with a missing timezone is in UTC
(timezone suffix "Z").
Fixes#562
Chooser dialog could start before any accounts have loaded. Fix by
collecting the account flow and waiting for the first emission (convert
the flow to shared instead of state so there's no initial empty list).
Guard against the potential for a similar issue when fetching
notifications.
Order the list of accounts with active account first so that code that
skips it by ignoring the first item works correctly.
Previous code was inconsistent about whether or not a notification toast
was shown after copying text (contrary to platform guidelines), and
there was some code duplication.
Fix this with a new `ClipboardUseCase` with a `copyTextTo` method that
handles copying text to the clipboard and showing a message afterwards
(depending on platform level).
Extend the "suggested accounts" accessibility actions to include any
mentions in the account's bio. Links, mentions, and hashtags are now
shown with a button to easily copy them.
Extend the "trending links" accessibility actions with a new "copy link"
action.
Consolidate common functionality in to the new
`PachliRecyclerviewAccessibilityDelegate` base class.
The copy button meant that some dialogs did not return the item click.
Fix this by having the adapter listen for clicks and forward them on.
Pre-emptively move the adapter to core.ui, as it's going to be useful
for the other accessiblity delegates.
Fixes#1108