Skip to content

feat: enhance exercise tracking, privacy settings, and bulk publishing features#173

Merged
isotronic merged 46 commits into
masterfrom
bug-fix
Jun 8, 2026
Merged

feat: enhance exercise tracking, privacy settings, and bulk publishing features#173
isotronic merged 46 commits into
masterfrom
bug-fix

Conversation

@isotronic

@isotronic isotronic commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Drag-and-drop reorder for tracked exercises in Stats
    • Privacy settings component and modal (Friends & Settings)
    • Rest timer overlay and improved timer controls
    • Social sync on startup to publish shared content
  • Improvements

    • Published badges shown for workouts/plans
    • Share UI now gated by plan/source and auth state
    • Progression suggestions refined for moderate effort
    • Android immersive workout mode; clearer set-complete button

isotronic added 30 commits June 3, 2026 10:52
Add ORDER BY te.sort_order ASC to the allTrackedRows query and replace
Object.values(groupedExercises) with an allTrackedRows-ordered map so
the returned array respects sort_order rather than numeric exercise_id order.
…orderTrackedExercises, atomicize tracked exercise save

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @isotronic, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4be1b08d-32a4-4c75-8a1a-d78abdade92c

📥 Commits

Reviewing files that changed from the base of the PR and between 6120e03 and 5485f74.

📒 Files selected for processing (9)
  • app/(app)/(tabs)/(plans)/index.tsx
  • components/PrivacySettings.tsx
  • components/RestTimerOverlay.tsx
  • hooks/useCreatePlan.ts
  • hooks/useCreateStandaloneWorkout.ts
  • hooks/useSocialSyncOnStartup.ts
  • locales/es/messages.po
  • utils/__tests__/progressionEngine.test.ts
  • utils/progressionEngine.ts
✅ Files skipped from review due to trivial changes (1)
  • locales/es/messages.po
🚧 Files skipped from review as they are similar to previous changes (5)
  • hooks/useCreateStandaloneWorkout.ts
  • components/RestTimerOverlay.tsx
  • components/PrivacySettings.tsx
  • hooks/useCreatePlan.ts
  • utils/tests/progressionEngine.test.ts

📝 Walkthrough

Walkthrough

Adds bulk-sharing and a startup sync, extracts PrivacySettings and integrates it into Friends/Settings, refactors workout timers and immersive mode, records feedback-submitted IDs to suppress progression suggestions, implements tracked-exercise reorder with DB migration, refactors progression logic, updates queries/tests, and refreshes localisation files.

Changes

Sharing, Feedback, Workout UX, Stats, and Progression

Layer / File(s) Summary
Bulk sharing and startup sync
utils/sharing.ts, hooks/useSocialSyncOnStartup.ts, utils/database.ts, app/(app)/_layout.tsx
New helpers to list publishable items, bulk-publish functions, and a startup hook that publishes missing items to Firestore based on privacy toggles.
Privacy settings component and integration
components/PrivacySettings.tsx, app/(app)/friends.tsx, app/(app)/settings.tsx
New PrivacySettings component with six share toggles and optional delete-all action; wired into Friends (modal) and Settings (inline).
Publish gating and published indicators
app/(app)/(tabs)/(plans)/index.tsx, components/StandaloneWorkoutListItem.tsx, hooks/usePlanPublishMutation.ts, hooks/useWorkoutPublishMutation.ts
Plans UI reads published IDs and threads isPublished into list items; publish cache keys now include user UID for per-user scoping.
Rest timer overlay and immersive mode
components/RestTimerOverlay.tsx, hooks/useWorkoutImmersiveMode.ts, app/(app)/(workout)/*
Replaces inline timer with RestTimerOverlay, adds useWorkoutImmersiveMode to hide Android nav bar while focused, and updates workout screens to use these components.
Feedback tracking and progression suppression
store/activeWorkoutStore.ts, app/(app)/(workout)/*, components/SessionSetInfo.tsx, components/ExerciseFeedbackSheet.tsx
Adds transient feedbackSubmittedUweIds and recordFeedbackSubmitted; progression suggestions suppressed once feedback recorded; feedback submission records UWE id before mutation.
Tracked-exercise reorder (UI, mutation, DB, migration)
app/(app)/(tabs)/(stats)/index.tsx, components/stats/ExerciseCompactCard.tsx, hooks/useReorderTrackedExercisesMutation.ts, utils/database.ts, utils/initUserDataDB.ts
Adds reorder mode with Sortable.Grid, reorder mutation, reorderTrackedExercises transactional DB updater, and migration/backfill for sort_order.
Tracked-exercises query and tests
hooks/useTrackedExercisesQuery.ts, hooks/__tests__/useTrackedExercisesQuery.test.ts
Parallel-fetches filtered and all tracked rows, seeds grouped result from all rows to preserve coverage when filtered sets are empty, adjusts tracking-type predicates, and updates tests with helpers.
Progression engine and tests
utils/progressionEngine.ts, utils/__tests__/progressionEngine.test.ts
Adds per-set rep-target computation, refactors easy/moderate on-target paths to actively progress (new MODERATE_TARGET_LOAD/REPS rules), and updates tests accordingly.
DB helpers and defaults
utils/database.ts, utils/initUserDataDB.ts
Adds fetch helpers for shareable IDs, reorderTrackedExercises, updates default settings for bilateral logging, and migration to add/backfill sort_order.
Firestore rules and small UI tweaks
firestore.rules, app/(app)/(tabs)/_layout.tsx, app/(app)/friend-profile.tsx
Shared-content read rules now allow owner or friends; menu tab shows pending-requests badge; friend “since” uses absolute locale date.
Localisation
locales/*/messages.po
Regenerated translation catalogs for de/en/es/fr to reflect UI updates.
Tests
utils/__tests__/*, hooks/__tests__/*
Added/updated tests for reorder mutation, DB helpers, sharing bulk publish, tracked-exercises query, and progression engine scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

"A rabbit peeks at code with a twitchy nose,
Publishing plans where the shared-data flows.
Timers that hover, reorder that sings,
Progression that nudges and tests with wings.
Hop on — the app's a brighter rose."

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
hooks/useCreatePlan.ts (1)

64-66: 💤 Low value

Consider invalidating the published plans query to keep UI consistent.

The fire-and-forget publishPlan call doesn't invalidate ["publishedPlanIds"] or ["planPublished", user.uid, newPlanId] queries. While startup sync will eventually reconcile state, matching the pattern in usePlanPublishMutation.ts:22-23 would keep the UI more consistent. However, the current approach is acceptable since the publish is async and the user may not immediately see the published indicator.

♻️ Optional refinement to align with mutation pattern

After the publishPlan call resolves, invalidate the relevant queries:

 if (user && privacySettings?.sharePlans) {
-  publishPlan(user.uid, newPlanId).catch((err) => Bugsnag.notify(err));
+  publishPlan(user.uid, newPlanId)
+    .then(() => {
+      queryClient.setQueryData(["planPublished", user.uid, newPlanId], true);
+      queryClient.invalidateQueries({ queryKey: ["publishedPlanIds"] });
+    })
+    .catch((err) => Bugsnag.notify(err));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useCreatePlan.ts` around lines 64 - 66, The fire-and-forget publishPlan
call in useCreatePlan.ts should invalidate the cached queries for
["publishedPlanIds"] and ["planPublished", user.uid, newPlanId] after it
completes to keep the UI consistent; update the publishPlan invocation to chain
a .then(() => { queryClient.invalidateQueries(["publishedPlanIds"]);
queryClient.invalidateQueries(["planPublished", user.uid, newPlanId]); }) and
retain the existing .catch(err => Bugsnag.notify(err)), mirroring the
invalidation pattern used in usePlanPublishMutation.ts (lines ~22-23) and
referencing publishPlan, ["publishedPlanIds"], and ["planPublished", user.uid,
newPlanId].
hooks/useCreateStandaloneWorkout.ts (1)

28-35: 💤 Low value

Consider invalidating the published workouts query to keep UI consistent.

Similar to the plan creation flow, this fire-and-forget publishStandaloneWorkout call doesn't invalidate the published workout queries. While functionally correct, matching the pattern in publish mutations would provide more immediate UI feedback. The current approach relies on startup sync for eventual consistency.

♻️ Optional refinement to align with mutation pattern

After the publishStandaloneWorkout call resolves, invalidate the relevant queries:

 onSuccess: (workoutId) => {
   queryClient.invalidateQueries({ queryKey: ["standaloneWorkouts"] });
   if (user && privacySettings?.shareStandaloneWorkouts) {
-    publishStandaloneWorkout(user.uid, workoutId).catch((err) =>
-      Bugsnag.notify(err),
-    );
+    publishStandaloneWorkout(user.uid, workoutId)
+      .then(() => {
+        queryClient.setQueryData(["workoutPublished", user.uid, workoutId], true);
+        queryClient.invalidateQueries({ queryKey: ["publishedWorkoutIds"] });
+      })
+      .catch((err) => Bugsnag.notify(err));
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useCreateStandaloneWorkout.ts` around lines 28 - 35, The onSuccess
handler for the create standalone workout mutation calls
publishStandaloneWorkout fire-and-forget but doesn't invalidate the
published-workouts cache; update the onSuccess in useCreateStandaloneWorkout so
that when user && privacySettings?.shareStandaloneWorkouts you await
publishStandaloneWorkout(user.uid, workoutId).then(() =>
queryClient.invalidateQueries({ queryKey: ["publishedWorkouts"] })).catch(err =>
Bugsnag.notify(err)); in short, call publishStandaloneWorkout and on its
resolution invalidate the publishedWorkouts query via
queryClient.invalidateQueries instead of only notifying on error.
hooks/useSocialSyncOnStartup.ts (1)

39-80: 💤 Low value

Remove redundant type annotations in map callbacks.

The type annotations (d: QDocSnap) on lines 46, 57, and 70 are unnecessary—TypeScript infers the type from snap.docs.map(...).

♻️ Simplify by removing explicit type annotations
-          const published = new Set(snap.docs.map((d: QDocSnap) => d.id));
+          const published = new Set(snap.docs.map((d) => d.id));

Apply the same change on lines 57 and 70.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useSocialSyncOnStartup.ts` around lines 39 - 80, The map callback type
annotations like (d: QDocSnap) and (ex: Exercise) are redundant because
TypeScript infers types from snap.docs.map(...) and exercises arrays; remove the
explicit annotations in the three map callbacks (the snap.docs.map(...) usages
and the exercises.filter(...) and missing.map(...) callbacks used alongside
publishPlan, publishStandaloneWorkout and pushCustomExercise) so the callbacks
become d => d.id and ex => ex.exercise_id checks (and ex =>
pushCustomExercise(uid, ex)), letting the compiler infer types.
utils/__tests__/progressionEngine.test.ts (1)

624-648: ⚡ Quick win

Add a reps-only ceiling regression test for easy sessions.

The updated suite validates reps-only progression when below max, but it still misses the “already at repsMax” case for trackingType: "reps". Adding that case will lock behaviour and prevent no-op increase_reps regressions.

✅ Suggested test addition
+  it("easy reps-only: holds when all sets are already at repsMax", () => {
+    const result = evaluateProgression(
+      makeInputs({
+        trackingType: "reps",
+        equipment: "body weight",
+        latestFeedback: makeFeedback({
+          effortRating: "easy",
+          performanceRatio: 1.0,
+        }),
+        recentWorkingWeight: null,
+        currentSets: [WORKING_SET],
+        completedRepsPerSet: [12],
+      }),
+    );
+    expect(result.action).toBe("hold");
+    expect(result.ruleKey).toBe("DEFAULT");
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/__tests__/progressionEngine.test.ts` around lines 624 - 648, The test
suite is missing a reps-only ceiling regression case: add a new unit test that
calls evaluateProgression with makeInputs using trackingType: "reps", a set
whose repsMax equals the completed reps (e.g., repsMin/repsMax both 10 or
completedRepsPerSet: [10] matching repsMax), and latestFeedback easy, then
assert the returned action is NOT "increase_reps" (expect no change or
appropriate ceiling action) and suggestedRepsPerSet either equals the current
completed reps or is undefined; reference the existing test that uses
evaluateProgression, makeInputs and makeFeedback to model the new test and
ensure it prevents regressions when at repsMax.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/`(app)/(tabs)/(plans)/index.tsx:
- Around line 164-167: The isPublished check is using the wrong privacy flag:
replace privacySettings?.sharePlans with
privacySettings?.shareStandaloneWorkouts in the isPublished expression (the
block where isPublished is set using
publishedWorkoutIds?.includes(String(item.id))). Update the conditional that
computes isPublished (referencing privacySettings, publishedWorkoutIds, and
item.id) so it uses shareStandaloneWorkouts to correctly reflect standalone
workout publishing (consistent with usePublishedWorkoutIdsQuery which reads
sharedStandaloneWorkouts).

In `@components/PrivacySettings.tsx`:
- Around line 45-63: The function handlePrivacyToggle updates local state and
calls updatePrivacy before verifying the user, which can cause updatePrivacy
(from usePrivacySettingsMutation) to throw when user is null; move the user
existence check (user) to the top of handlePrivacyToggle so you return early if
no user, then perform setLocalPrivacySettings and updatePrivacy only after
confirming user is defined, and keep the subsequent conditional
bulkPublishAllPlans, bulkPublishAllStandaloneWorkouts, and
bulkPublishAllCustomExercises calls unchanged but also guarded by the confirmed
user.

In `@components/RestTimerOverlay.tsx`:
- Around line 90-106: The style in createStyles currently uses the web-only
boxShadow property; replace it with React Native shadow props for iOS and keep
elevation for Android: remove boxShadow and add shadowColor (use a color from
colors, e.g., colors.shadow or colors.card with alpha), shadowOffset (width: 0,
height: -2), shadowOpacity (e.g., 0.3), and shadowRadius (e.g., 4) inside the
container style so the overlay shows a cross-platform shadow while preserving
elevation.
- Around line 15-20: Replace the incorrect use of "typeof LayoutChangeEvent"
with the LayoutChangeEvent type itself in the AnimatedView prop typing and the
related interface (remove the typeof operator), and ensure LayoutChangeEvent is
imported from 'react-native' so the signatures read onLayout?: (event:
LayoutChangeEvent) => void; (do this for both the AnimatedView declaration and
the interface at the other occurrence).

In `@locales/es/messages.po`:
- Around line 1716-1724: Replace the current translations for the
"Dismiss"/"DISMISS" msgid entries so they use an action verb that conveys
rejection rather than closing: change msgstr "Cerrar"/"CERRAR" to
"Descartar"/"DESCARTAR" for the entries used by ProgressionSummaryCard.tsx,
UpdateModal.tsx and app/(app)/(tabs)/(plans)/overview.tsx so the UI indicates
rejecting a suggestion rather than merely closing a modal.

In `@utils/progressionEngine.ts`:
- Around line 274-285: The branch handling trackingType === "reps" must avoid
emitting an "increase_reps" suggestion when computePerSetRepTargets(...) returns
targets identical to the current set reps (i.e., at the rep ceiling); change the
logic in that branch (where perSetTargets is computed) to compare perSetTargets
against the current workingSets reps and only return the increase_reps response
(with ruleKey "EASY_TARGET_REPS") if at least one suggested rep is greater than
the corresponding workingSets rep; otherwise return hold("DEFAULT"). Use the
existing symbols computePerSetRepTargets, perSetTargets, workingSets, and hold
to locate and implement this check.

---

Nitpick comments:
In `@hooks/useCreatePlan.ts`:
- Around line 64-66: The fire-and-forget publishPlan call in useCreatePlan.ts
should invalidate the cached queries for ["publishedPlanIds"] and
["planPublished", user.uid, newPlanId] after it completes to keep the UI
consistent; update the publishPlan invocation to chain a .then(() => {
queryClient.invalidateQueries(["publishedPlanIds"]);
queryClient.invalidateQueries(["planPublished", user.uid, newPlanId]); }) and
retain the existing .catch(err => Bugsnag.notify(err)), mirroring the
invalidation pattern used in usePlanPublishMutation.ts (lines ~22-23) and
referencing publishPlan, ["publishedPlanIds"], and ["planPublished", user.uid,
newPlanId].

In `@hooks/useCreateStandaloneWorkout.ts`:
- Around line 28-35: The onSuccess handler for the create standalone workout
mutation calls publishStandaloneWorkout fire-and-forget but doesn't invalidate
the published-workouts cache; update the onSuccess in useCreateStandaloneWorkout
so that when user && privacySettings?.shareStandaloneWorkouts you await
publishStandaloneWorkout(user.uid, workoutId).then(() =>
queryClient.invalidateQueries({ queryKey: ["publishedWorkouts"] })).catch(err =>
Bugsnag.notify(err)); in short, call publishStandaloneWorkout and on its
resolution invalidate the publishedWorkouts query via
queryClient.invalidateQueries instead of only notifying on error.

In `@hooks/useSocialSyncOnStartup.ts`:
- Around line 39-80: The map callback type annotations like (d: QDocSnap) and
(ex: Exercise) are redundant because TypeScript infers types from
snap.docs.map(...) and exercises arrays; remove the explicit annotations in the
three map callbacks (the snap.docs.map(...) usages and the exercises.filter(...)
and missing.map(...) callbacks used alongside publishPlan,
publishStandaloneWorkout and pushCustomExercise) so the callbacks become d =>
d.id and ex => ex.exercise_id checks (and ex => pushCustomExercise(uid, ex)),
letting the compiler infer types.

In `@utils/__tests__/progressionEngine.test.ts`:
- Around line 624-648: The test suite is missing a reps-only ceiling regression
case: add a new unit test that calls evaluateProgression with makeInputs using
trackingType: "reps", a set whose repsMax equals the completed reps (e.g.,
repsMin/repsMax both 10 or completedRepsPerSet: [10] matching repsMax), and
latestFeedback easy, then assert the returned action is NOT "increase_reps"
(expect no change or appropriate ceiling action) and suggestedRepsPerSet either
equals the current completed reps or is undefined; reference the existing test
that uses evaluateProgression, makeInputs and makeFeedback to model the new test
and ensure it prevents regressions when at repsMax.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4b756ad2-c4c9-4489-9a59-bc8ed97ce494

📥 Commits

Reviewing files that changed from the base of the PR and between 7e69f47 and 6120e03.

📒 Files selected for processing (49)
  • app/(app)/(tabs)/(plans)/index.tsx
  • app/(app)/(tabs)/(plans)/overview.tsx
  • app/(app)/(tabs)/(plans)/standalone-workout.tsx
  • app/(app)/(tabs)/(stats)/exercises.tsx
  • app/(app)/(tabs)/(stats)/index.tsx
  • app/(app)/(tabs)/_layout.tsx
  • app/(app)/(workout)/index.tsx
  • app/(app)/(workout)/workout-session.tsx
  • app/(app)/_layout.tsx
  • app/(app)/friend-profile.tsx
  • app/(app)/friends.tsx
  • app/(app)/settings.tsx
  • components/ExerciseFeedbackSheet.tsx
  • components/PrivacySettings.tsx
  • components/ProgressionSuggestionChip.tsx
  • components/ProgressionSummaryCard.tsx
  • components/RestTimerOverlay.tsx
  • components/SessionSetInfo.tsx
  • components/StandaloneWorkoutListItem.tsx
  • components/TrainingPlanCard.tsx
  • components/stats/ExerciseCompactCard.tsx
  • firestore.rules
  • hooks/__tests__/useReorderTrackedExercisesMutation.test.ts
  • hooks/__tests__/useTrackedExercisesQuery.test.ts
  • hooks/useCreatePlan.ts
  • hooks/useCreateStandaloneWorkout.ts
  • hooks/useExerciseDetailQuery.ts
  • hooks/usePlanPublishMutation.ts
  • hooks/useReorderTrackedExercisesMutation.ts
  • hooks/useSocialSyncOnStartup.ts
  • hooks/useTrackedExercisesQuery.ts
  • hooks/useWorkoutImmersiveMode.ts
  • hooks/useWorkoutPublishMutation.ts
  • locales/de/messages.js
  • locales/de/messages.po
  • locales/en/messages.js
  • locales/en/messages.po
  • locales/es/messages.js
  • locales/es/messages.po
  • locales/fr/messages.js
  • locales/fr/messages.po
  • store/activeWorkoutStore.ts
  • utils/__tests__/database.test.ts
  • utils/__tests__/progressionEngine.test.ts
  • utils/__tests__/sharing.test.ts
  • utils/database.ts
  • utils/initUserDataDB.ts
  • utils/progressionEngine.ts
  • utils/sharing.ts

Comment thread app/(app)/(tabs)/(plans)/index.tsx
Comment thread components/PrivacySettings.tsx
Comment thread components/RestTimerOverlay.tsx
Comment thread components/RestTimerOverlay.tsx
Comment thread locales/es/messages.po Outdated
Comment thread utils/progressionEngine.ts
@isotronic isotronic merged commit 0e279d8 into master Jun 8, 2026
3 checks passed
@isotronic isotronic deleted the bug-fix branch June 8, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant