Skip to content

fix(media-list): preserve ordering, viewport, notes field, and next-airing state on entry updates - #1131

Open
wax911 wants to merge 4 commits into
developfrom
fix/media-list-update-bugs
Open

fix(media-list): preserve ordering, viewport, notes field, and next-airing state on entry updates#1131
wax911 wants to merge 4 commits into
developfrom
fix/media-list-update-bugs

Conversation

@wax911

@wax911 wax911 commented Sep 12, 2026

Copy link
Copy Markdown
Member

AniTrend Pull Request

Thank you for contributing! Please take a moment to review our contributing guidelines
to make the process easy and effective for everyone involved.

Please open an issue before embarking on any significant pull request, especially those that
add a new library or change existing tests, otherwise you risk spending a lot of time working
on something that might not end up being merged into the project.

Before opening a pull request, please ensure you've done the following:

  • You have followed our contributing guidelines
  • Double checked that your branch is based on develop and targets develop (where applicable)
  • Pull request has tests (if applicable)
  • Documentation is updated (if necessary)
  • Description explains the issue/use-case resolved

Description

Fixes four reported regressions in the media-list entry update flow. Branch is based on and targets develop.

1. Visible list order changed after an entry update.
InMemoryMediaListStore.updateQueryMembership removed the updated entry ID and re-appended it, which reshuffled API-preserved ties and moved rows even when the active sort was server-ordered. The updated ID now keeps its slot before a stable re-sort, and ADDED_TIME / UPDATED_TIME comparators use the server createdAt / updatedAt values (added to MediaListCoreFragment, MediaListRecord, and the collection and mutation mappers) instead of the local request revision. revision remains only the stale-response guard.

2. Viewport moved away from the edited item.
BottomSheetSeriesManage now emits mutation-started and mutation-failed fragment results keyed by media ID. MediaListFragment captures the first visible media ID, decorated top offset, edited-entry baseline revision, and predecessor identities before a mutation, waits until the store commits a higher revision or the target leaves the active filter, and restores the recorded anchor through the adapter's final submitList commit callback (after the two-stage filter + diff). Failed mutations clear only the matching pending anchor. Shared increment dispatch (used by AiringListFragment) captures the same anchor. No notifyDataSetChanged, no object-identity keys, no new KeyUtil.*_REQ routing.

3. Comment placeholder overlapped entered text.
The notes card rendered both a static section_notes label and the outlined text-field hint. The duplicate MaterialTextView is removed; the TextInputLayout hint (dialog_hint_enter_your_notes) is the single placeholder owner, consistent with the BottomSheetSeriesManage reference design.

4. Next-airing label stayed at Releasing until app restart.
When a successful save returns media without nextAiringEpisode (observed for RELEASING titles), SaveMediaListEntryInteractor now performs a serial getMediaWithList fetch by the authoritative mediaId, re-validates the mutation session, and merges only the refreshed nextAiringEpisode into the committed record in the same upsert. A failed refresh does not invalidate the committed mutation, and no client-side dates are synthesized.

Invariants and compatibility

  • Server-authoritative ordering: existing IDs keep their slot before a stable re-sort; equal-key rows keep API order; no client-side timestamp guessing.
  • Viewport preservation is anchored to stable media IDs and pixel offsets, restored once, then cleared; stale anchors are dropped on matching mutation failure.
  • Mutation session is re-verified after the refresh await; stale refreshed data cannot commit.
  • The refresh merge is field-preserving (copy of the existing summary), so mutation-supplied fields such as banner image and site URL are retained.
  • No presenter usage added; all touched paths are the migrated store/ViewModel surface. Legacy RecyclerViewAdapter and KeyUtil.*_REQ routing untouched.

Files changed

  • app/src/main/graphql/fragments/medialist/MediaListCoreFragment.graphql (add createdAt)
  • app/src/main/java/com/mxt/anitrend/domain/medialist/model/MediaListRecord.kt
  • app/src/main/java/com/mxt/anitrend/data/mapper/MediaListRecordMapper.kt
  • app/src/main/java/com/mxt/anitrend/data/mapper/MediaListCollectionRecordMapper.kt
  • app/src/main/java/com/mxt/anitrend/repository/mapper/BrowseMapper.kt
  • app/src/main/java/com/mxt/anitrend/data/store/medialist/InMemoryMediaListStore.kt
  • app/src/main/java/com/mxt/anitrend/domain/medialist/interactor/SaveMediaListEntryInteractor.kt
  • app/src/main/java/com/mxt/anitrend/adapter/recycler/index/MediaListAdapter.kt
  • app/src/main/java/com/mxt/anitrend/view/fragment/list/MediaListFragment.kt
  • app/src/main/java/com/mxt/anitrend/view/sheet/BottomSheetSeriesManage.kt
  • app/src/main/res/layout/sheet_series_manage_m3.xml
  • Tests: MediaListStoreTest.kt, SaveMediaListEntryInteractorTest.kt, BrowseMediaListCollectionRepositoryTest.kt (fixture field)

Tests added

  • entry update reorders added-time sorts using server timestamps (red-first: failed before comparator fix)
  • entry update reorders updated-time sorts using server timestamps (red-first)
  • entry update preserves API order for equal updated-time keys (red-first: thenBy(mediaId) tie-break reshuffle)
  • successful save with missing airing data commits refreshed next-airing episode (red-first: failed pre-fix with expected 12 but was null)
  • Existing suite passes unchanged apart from the updated repository fixture.

Verification commands executed

  • ./gradlew :app:testAppDebugUnitTest --tests com.mxt.anitrend.data.store.MediaListStoreTest --tests com.mxt.anitrend.domain.medialist.interactor.SaveMediaListEntryInteractorTest (all pass)
  • ./gradlew test --no-daemon (full unit suite, all pass)
  • ./gradlew :app:spotlessCheck --no-daemon (pass)
  • ./gradlew :app:assembleAppDebug :app:assembleGithubDebug --no-daemon (pass, APKs produced)
  • CI-equivalent: spotlessCheck test plus both flavor builds exit 0

Known limitations

  • Live on-device verification of the four flows was interrupted (AniList returned HTTP 403 "API temporarily disabled" during the session and the runtime pass was stopped mid-run); all four fixes are covered by the offline red-green unit regressions above. AniList API reachability was re-confirmed (HTTP 200) at session end.
  • When neither the mutation payload nor the fallback refresh returns nextAiringEpisode, the row keeps Releasing; no client-side date is guessed.

Follow-ups

  • Reconcile app/.meta/version.json minSdk mismatch (existing automation issue, untouched here).
  • Viewport restoration could move into a shared list-render helper if a second screen needs the same contract.

Rollback procedure

Revert the four commits (d585aaa42, b637c2ce6, a54cf8555, 9255401e5); no schema, migration, or persisted-state changes are involved, so a plain revert restores prior behavior.

Screenshots:

Live UI captures pending; offline verification evidence is in the description above.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Enhancement (Improves existing functionality)

IMPORTANT: By submitting a patch, you agree to allow the project
owners to license your work under the terms of the MIT License.

Updates to a list entry no longer remove and re-append its ID, which reshuffled API-preserved ties. The updated ID keeps its slot before a stable re-sort, ADDED_TIME and UPDATED_TIME now compare the server createdAt/updatedAt carried by MediaListCoreFragment instead of the local request revision, and revision remains the stale-response guard only.
The manage sheet now emits mutation-started and mutation-failed fragment results keyed by media ID. MediaListFragment captures the first visible media ID, decorated top offset, target baseline revision, and predecessor identities before a mutation, waits for the store to commit a higher revision or drop the target from the filter, then restores the recorded anchor through the adapter final submitList commit callback. Failed mutations clear only the matching pending anchor, and shared increment dispatch (including AiringListFragment) captures the same anchor.
The section label and the TextInputLayout hint rendered the same prompt and overlapped entered text. The static MaterialTextView is removed so the outlined text field hint is the single placeholder owner, matching the BottomSheetSeriesManage reference design.
When a successful save returns media without nextAiringEpisode (seen on RELEASING titles), the interactor now fetches getMediaWithList by the authoritative mediaId, re-validates the mutation session, and merges only the refreshed nextAiringEpisode into the committed record, so the row leaves Releasing without an app restart. Failed refreshes do not invalidate the committed mutation.
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 24 complexity · 0 duplication

Metric Results
Complexity 24
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant