Review pass on the detach path. Three changes, none to the write shape itself.
A second detach of the same occurrence is now refused. Reached from a stale
screen still pointing at the parent, it used to succeed twice over: the EXDATE
merge folds the repeated stamp away and the parent update still reports one row
changed, so the save looked fine and left a *second* standalone copy of one
occurrence, this time with neither copy in the series to make it obvious. The
occurrence is already gone from the series at that point, so the write reports
it as gone — and EventEditViewModel now maps NoSuchEventException from the write
to the same "this event no longer exists" answer its pre-check already gives,
instead of a bare "couldn't save" for something that isn't there.
The rollback comment claimed more than the code delivers. ContentResolver.update
returns rows touched, not occurrences excluded, so the zero check catches the
series row vanishing mid-write and nothing else — an EXDATE the provider's
expansion fails to match reports success and leaves the duplicate standing.
Renamed the variable to match, and the catch now mirrors moveEvent's Throwable
idiom rather than inventing a narrower one for the same insert-then-roll-back
shape.
The rest is honesty about what a detached row can't do, since none of it is
recoverable later and the KDoc previously mentioned only the calendar-move case:
a whole-series delete leaves it standing where an exception row would have gone
with the parent; a series-wide *time* edit moves the generated instances but not
the absolute-instant EXDATE hole, so the occurrence returns alongside its copy;
and building the row from the form rather than cloning the parent drops
ORGANIZER, STATUS and the organizer/resource attendee rows, exactly as moveEvent
does. The EXDATE staleness is not new — a #47 delete resurrects the same way —
but a duplicate is a louder symptom than a resurrection, and it wants fixing at
the series-update end, not here.
Also notes why the branch predicate is stricter than deleteOccurrence's: EXDATE
only means something on a row that recurs, so a row without an RRULE keeps the
old path rather than having a recurrence set written onto a one-off event.
Tests: the tautological "detached == copy(rrule = null)" assertion is replaced by
one that pins every edited field through to the inserted columns, and
exdateContains gets its own cases (timed, all-day, absent, a neighbouring
occurrence, and a multi-entry list with whitespace).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Edit only this event" wrote a modified-occurrence exception unconditionally.
That is the right shape for a synced series and the wrong one for everything
else: an exception attaches to its parent through ORIGINAL_SYNC_ID, so on a row
with no _sync_id the link never forms — the insert fails or lands an orphan, the
generic catch in EventEditViewModel turns it into a snackbar, and the scope
dialog just closes again. To the reporter that read as "nothing happens, ever",
with a stray copy of the event left behind the one time the insert did land.
This is the same constraint deleteOccurrence has documented since #47, and the
same calendars: a local calendar, Calendula's own contact special-date
calendars, and — the reporter's case — a Google calendar whose rows the sync
adapter has not stamped yet, which is exactly why it "shows as on-device".
updateOccurrence now branches on _sync_id the way deleteOccurrence does. A
synced series keeps the exception path untouched; its write shape is load-
bearing and verified. A series without one gets the occurrence excluded from the
parent via EXDATE and the edited values inserted as a standalone event on the
same calendar — a detached instance, minus the RECURRENCE-ID the provider has no
way to store here. The cost is honest and worth naming: the edited occurrence
stops travelling with its series. The alternative is an edit that silently does
nothing.
Two things shape the write. The parent update reuses buildOccurrenceExdateValues
unchanged, so it keeps carrying the whole time/recurrence set — an EXDATE-only
update is not a recurrence change to the provider and leaves the expanded
instances standing (#47's first quirk). And the form's RRULE is stripped before
the insert: the exception path gets an inherited rule cleared for free by
DTSTART + DURATION, but nothing clears one here, so leaving it would insert a
second *series* overlapping the first.
Ordering is chosen for the failure cases, not the happy path. The insert runs
first, so a failure there leaves the series completely untouched — the discipline
updateEventFromOccurrence already follows. If the EXDATE update then fails, the
new row is a visible duplicate of an occurrence still in the series, so it is
rolled back (best effort) before the failure surfaces. The reverse order could
strand an occurrence excluded from its series with nothing standing in for it,
turning an edit into a silent delete.
Also makes a failed save legible, since this bug was invisible precisely because
it wasn't: the failure snackbar gets the long duration instead of a flash, and
the catch logs the scope and event id — never the form's content — so a failure
leaves something to report.
Adds JVM tests for the detached shape: the rule is dropped and the row becomes a
one-off with DTEND, all-day stays on UTC midnights, and the detached row's
DTSTART agrees with the EXDATE stamp that removes it from the parent, timed and
all-day. The provider behaviour itself still needs a device.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Surface the community translations that landed via Weblate but were never
listed in locales_config.xml. European Portuguese stays out: it has one
translated string.
Release 2.19.2 — `versionName` 2.19.2 / `versionCode` 21902. Merging this cuts the release.
A meeting you declined is struck through everywhere it appears and no longer schedules a reminder (#180). Tapping an event in month view opens the event instead of its day, in every month style (#187). An edit shows immediately when the same occurrence is re-opened (#196).
The month widget's arrows died after a couple of taps: the grid serialised ~740 views and one update ran to 450–540 KB, which overran the launcher's async binder buffer and made Android drop the whole widget host — killing updates for every widget on the home screen. The grid is down to 192 views, and `SizeMode.Exact` is kept so a resized widget reflows instead of clipping (#214). Widget taps now redraw the tapped widget by its own id rather than `updateAll`, whose provider lookup is empty in a cold-woken process (#18).
Jump-to-today in seamless weeks lands on the current week row (#191), and the day and week timelines got an end inset (#192).
Two follow-ups beyond the issues: the detail re-read keeps the loaded content on screen instead of blinking back to the skeleton, and a month chip tap with no recorded touch down (TalkBack, D-pad) no longer resolves to the previous tap's chip.
Release notes: `CHANGELOG.md` 2.19.2 section and `fastlane/metadata/android/en-US/changelogs/21902.txt` (471 chars).
Closes#18Closes#180Closes#187Closes#191Closes#192Closes#196Closes#214
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/216
Release branch for 2.19.1. Merging this cuts the release — versionName is bumped to 2.19.1 (versionCode 21901), which is what release.yaml picks up.
### What's in it
- **Drag to reschedule can be turned off** (#173). Settings → Views → *Drag to reschedule*, on by default. Turned off, no event block in the month, week or day view registers a drag gesture. The gate is `EventMoveScope.dragEnabled` rather than the scope itself, so the screen-reader "Move event" action — which only opens the edit form — stays available either way. The setting reads as off until the stored value has actually loaded, so a cold start can't hand out a drag before the preference is known.
### Release prep
- CHANGELOG 2.19.1 section written.
- Per-version "What's New" hand-written for all 12 store locales (`ar`, `de-DE`, `en-GB`, `en-US`, `es-ES`, `fr-FR`, `it-IT`, `pl-PL`, `pt-BR`, `pt-PT`, `ru-RU`, `zh-CN`).
- `lint test assembleDebug` and `check_translations.py` green locally.
- main is one commit ahead (Weblate #175, translations only) and merges clean — not merged into the branch, it comes along with this one.
Closes#173
Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/176