Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
b6ac253
fix: translations
isotronic Jun 1, 2026
8011361
chore: add docs/ directory to .gitignore
isotronic Jun 1, 2026
a966448
chore: install bugsnag performance monitoring packages
isotronic Jun 1, 2026
db51832
feat: initialize bugsnag performance with navigation plugin
isotronic Jun 1, 2026
b188db7
feat: register expo router navigation ref with bugsnag performance
isotronic Jun 1, 2026
67ecf78
feat: wrap app root with bugsnag performance app start instrumentation
isotronic Jun 1, 2026
a2f6576
refactor: inline bugsnag performance nav plugin instantiation
isotronic Jun 1, 2026
01f474f
chore: install @react-native-firebase/firestore
isotronic Jun 1, 2026
9786090
feat: add Firestore TypeScript document types
isotronic Jun 1, 2026
4d396e4
feat: add Firestore security rules
isotronic Jun 1, 2026
888f382
feat: upsert Firestore user profile on sign-in
isotronic Jun 1, 2026
d91eb50
feat: add Zustand social store
isotronic Jun 1, 2026
419d0e1
feat: add real-time Firestore listeners for friends and requests
isotronic Jun 1, 2026
55de8c9
feat: add friend request and removal Firestore operations
isotronic Jun 1, 2026
a8c03e1
feat: add friend mutation hooks
isotronic Jun 1, 2026
92bafea
feat: add FriendListItem and FriendRequestItem components
isotronic Jun 1, 2026
bbd8c11
feat: add Friends item to AppMenu with pending request badge
isotronic Jun 1, 2026
d12ef90
feat: add Friends screen with tabbed navigation
isotronic Jun 1, 2026
8c23fed
fix: address code review issues in friends phase 1
isotronic Jun 1, 2026
e0df82e
test: update query configurations and add default values for deload
isotronic Jun 1, 2026
406e579
feat: add sharing types, privacy settings to social store and listener
isotronic Jun 1, 2026
85649d4
feat: add DB helpers for sharing plan, workout, measurement, and PR data
isotronic Jun 1, 2026
a981473
feat: add sharing utility with Firestore write/delete functions
isotronic Jun 1, 2026
0e4cf9f
feat: add privacy settings toggles and delete shared data to Settings…
isotronic Jun 1, 2026
aeb94b7
feat: add per-plan share toggle to plan overview screen
isotronic Jun 1, 2026
747f106
feat: auto re-publish plan to Firestore when an already-shared plan i…
isotronic Jun 1, 2026
f56ffd7
feat: add standalone workout publish hooks
isotronic Jun 1, 2026
c683c8b
feat: add standalone workout share toggle and auto re-publish on edit
isotronic Jun 1, 2026
6f862a1
feat: add published plan/workout IDs queries
isotronic Jun 1, 2026
1894e36
feat: add isPublished prop and cloud icon to plan card components
isotronic Jun 1, 2026
56338da
feat: pass published plan IDs to plan list for cloud icon display
isotronic Jun 1, 2026
da13d0c
feat: auto-push custom exercises to Firestore when share toggle is on
isotronic Jun 1, 2026
c3eaa6b
feat: auto-push completed workouts and strength PRs to Firestore
isotronic Jun 1, 2026
1fd0d8c
feat: auto-push body measurements to Firestore when share toggle is on
isotronic Jun 1, 2026
b7ddb16
test: add Firestore/AuthContext/sharing mocks for hooks affected by P…
isotronic Jun 1, 2026
a075980
feat: export ensureAppExercisesExist for use by import utilities
isotronic Jun 1, 2026
7009694
feat: add resolveExerciseId utility for plan/workout import
isotronic Jun 1, 2026
c6c13f5
feat: add friend shared content query hooks for all six Firestore sub…
isotronic Jun 1, 2026
ee6e87d
feat: add import mutations for plans, standalone workouts, and custom…
isotronic Jun 1, 2026
a52a02a
feat: add friend profile and detail screens with import buttons
isotronic Jun 1, 2026
165e903
fix: include user ID in query keys for published plan and workout IDs
isotronic Jun 1, 2026
cdfa0a1
refactor: streamline publish and unpublish functions for plans and st…
isotronic Jun 1, 2026
ae204dd
feat: add user feedback for deleting data and improve error handling
isotronic Jun 1, 2026
c4fa0a1
feat: enhance friend search functionality to display incoming requests
isotronic Jun 1, 2026
f1f00ab
feat: add error handling for exercise import with alert notifications
isotronic Jun 1, 2026
5931f9a
fix: handle optional workout update date and improve exercise rep dis…
isotronic Jun 1, 2026
6839b63
feat: add error message for sharing workout failure and improve UI st…
isotronic Jun 1, 2026
8f372b0
fix: improve error message for unauthenticated user in privacy settin…
isotronic Jun 1, 2026
5da7efe
fix: update exists checks to use the correct method for Firestore doc…
isotronic Jun 1, 2026
cfcf337
fix: update friends subcollection rules to improve write permissions …
isotronic Jun 1, 2026
d0a6143
fix: enhance friend request handling by adding pending status check a…
isotronic Jun 2, 2026
48cfd4f
fix: optimize pushStrengthPRs by batching Firestore writes and simpli…
isotronic Jun 2, 2026
9304c59
fix: clarify friend creation rules in Firestore security rules
isotronic Jun 2, 2026
0111278
chore: update package.json dependencies and scripts
isotronic Jun 2, 2026
22da128
fix: enhance friend request validation by adding UID checks and refin…
isotronic Jun 2, 2026
f73009e
chore: update app version and version code to 1.3.0 and 10300 respect…
isotronic Jun 2, 2026
0f21e56
feat: add new translations
isotronic Jun 2, 2026
e1ea151
feat: add Friends & Social sharing features to WhatsNew and HelpData
isotronic Jun 2, 2026
1051fe4
refactor: optimize hasPendingRequest function
isotronic Jun 2, 2026
7d72e80
chore: correct version and versionCode comment formatting in app.conf…
isotronic Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ expo-env.d.ts
# @end expo-cli

coverage/
docs/
.claude/
*copilot-instructions.md
*EXPO_SDK_52_UPGRADE_PLAN.md
.firebaserc
4 changes: 2 additions & 2 deletions app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
expo: {
name: IS_DEV ? "Muscle Quest (Dev)" : "Muscle Quest",
slug: "musclequest",
version: "1.0.0", // MM.mm.pp
version: "1.3.0", // M.mm.pp
orientation: "portrait",
icon: "./assets/images/icon.png",
scheme: "musclequest",
Expand All @@ -14,7 +14,7 @@ export default {
bundleIdentifier: "com.isotronic.musclequest",
},
android: {
versionCode: 10000, // MMmmpp
versionCode: 10300, // Mmmpp
googleServicesFile: "./google-services.json",
adaptiveIcon: {
foregroundImage: "./assets/images/ic_launcher_foreground.png",
Expand Down
6 changes: 6 additions & 0 deletions app/(app)/(tabs)/(plans)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { Trans } from "@lingui/react/macro";
import { t } from "@lingui/core/macro";
import { useAppTheme } from "@/theme";
import type { AppThemeColors } from "@/theme/types";
import { usePublishedPlanIdsQuery } from "@/hooks/usePublishedPlanIdsQuery";
import { useSocialStore } from "@/store/socialStore";

export default function PlansScreen() {
const { colors } = useAppTheme();
Expand All @@ -35,6 +37,8 @@ export default function PlansScreen() {
isError: standaloneIsError,
error: standaloneError,
} = useStandaloneWorkoutsQuery();
const { data: publishedPlanIds } = usePublishedPlanIdsQuery();
const { privacySettings } = useSocialStore();

useEffect(() => {
if (standaloneIsError && standaloneError) {
Expand Down Expand Up @@ -129,6 +133,8 @@ export default function PlansScreen() {
viewMode={viewMode}
showViewToggle
onViewModeChange={handleViewModeChange}
publishedPlanIds={publishedPlanIds}
sharePlansEnabled={!!privacySettings?.sharePlans}
/>
<PlanList
title={t`Premade plans`}
Expand Down
47 changes: 46 additions & 1 deletion app/(app)/(tabs)/(plans)/overview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState, useCallback } from "react";
import { useMemo, useState, useCallback, useContext } from "react";
import {
View,
StyleSheet,
Expand Down Expand Up @@ -38,6 +38,10 @@ import { useDeloadWeekQuery } from "@/hooks/useDeloadWeekQuery";
import { useDeloadWeekMutation } from "@/hooks/useDeloadWeekMutation";
import { useProgressionSettingsQuery } from "@/hooks/useProgressionSettingsQuery";
import { getCurrentISOWeek } from "@/utils/isoWeek";
import { AuthContext } from "@/context/AuthProvider";
import { useSocialStore } from "@/store/socialStore";
import { usePlanPublishQuery } from "@/hooks/usePlanPublishQuery";
import { usePlanPublishMutation } from "@/hooks/usePlanPublishMutation";

const fallbackImage = require("@/assets/images/placeholder.webp");

Expand Down Expand Up @@ -100,6 +104,13 @@ export default function PlanOverviewScreen() {
);
const deloadMutation = useDeloadWeekMutation(Number(planId));

const user = useContext(AuthContext);
const { privacySettings } = useSocialStore();
const showShareToggle = !!user && !!privacySettings?.sharePlans;
const { data: isPublished = false, isLoading: isPublishLoading } =
usePlanPublishQuery(showShareToggle ? Number(planId) : null);
const publishMutation = usePlanPublishMutation(Number(planId));

const handleToggleDeload = useCallback(() => {
deloadMutation.mutate(isCurrentWeekDeload ? null : getCurrentISOWeek());
}, [isCurrentWeekDeload, deloadMutation]);
Expand Down Expand Up @@ -270,6 +281,40 @@ export default function PlanOverviewScreen() {
</View>
</TouchableOpacity>
)}

{showShareToggle && (
<TouchableOpacity
onPress={() => publishMutation.mutate(!isPublished)}
style={[styles.deloadRow]}
activeOpacity={0.7}
disabled={publishMutation.isPending || isPublishLoading}
>
<View style={styles.deloadLeft}>
<AppIcon
set="mci"
name="cloud-outline"
size={20}
color={isPublished ? colors.accent : colors.contentSecondary}
style={{ marginRight: 10 }}
/>
<ThemedText
style={[
styles.deloadTitle,
isPublished && { color: colors.accent },
]}
>
<Trans>Share Plan</Trans>
</ThemedText>
</View>
{publishMutation.isPending || isPublishLoading ? (
<ActivityIndicator size="small" color={colors.accent} />
) : (
<View pointerEvents="none">
<Switch value={isPublished} color={colors.accent} />
</View>
)}
</TouchableOpacity>
)}
</ScrollView>

<View style={styles.buttonContainer}>
Expand Down
79 changes: 77 additions & 2 deletions app/(app)/(tabs)/(plans)/standalone-workout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useContext, useMemo, useState } from "react";
import {
View,
StyleSheet,
Expand All @@ -10,13 +10,14 @@ import { ThemedText } from "@/components/ThemedText";
import { ThemedView } from "@/components/ThemedView";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { UserExercise } from "@/store/workoutStore";
import { AppImage } from "@/components/ui";
import { AppIcon, AppImage } from "@/components/ui";
import {
ActivityIndicator,
Button,
IconButton,
Portal,
Modal,
Switch,
} from "react-native-paper";
import { Notes } from "@/components/Notes";
import { byteArrayToBase64, formatFromTotalSeconds } from "@/utils/utility";
Expand All @@ -30,6 +31,10 @@ import { Trans } from "@lingui/react/macro";
import { t } from "@lingui/core/macro";
import { useAppTheme, radii } from "@/theme";
import type { AppThemeColors } from "@/theme/types";
import { AuthContext } from "@/context/AuthProvider";
import { useSocialStore } from "@/store/socialStore";
import { useWorkoutPublishQuery } from "@/hooks/useWorkoutPublishQuery";
import { useWorkoutPublishMutation } from "@/hooks/useWorkoutPublishMutation";

const fallbackImage = require("@/assets/images/placeholder.webp");

Expand All @@ -47,6 +52,13 @@ export default function StandaloneWorkoutScreen() {
const { data: settings } = useSettingsQuery();
const distanceUnit = settings?.distanceUnit || "m";

const user = useContext(AuthContext);
const { privacySettings } = useSocialStore();
const showShareToggle = !!user && !!privacySettings?.shareStandaloneWorkouts;
const { data: isPublished = false, isLoading: isPublishLoading } =
useWorkoutPublishQuery(showShareToggle ? workoutId : null);
const publishMutation = useWorkoutPublishMutation(workoutId);

if (!Number.isInteger(workoutId) || workoutId <= 0) {
return (
<ThemedView style={styles.centered}>
Expand Down Expand Up @@ -254,6 +266,46 @@ export default function StandaloneWorkoutScreen() {
) : (
workout.exercises.map((exercise) => renderExercise(exercise))
)}
{showShareToggle && (
<>
<TouchableOpacity
onPress={() => publishMutation.mutate(!isPublished)}
style={styles.shareRow}
activeOpacity={0.7}
disabled={publishMutation.isPending || isPublishLoading}
>
<View style={styles.shareLeft}>
<AppIcon
set="mci"
name="cloud-outline"
size={20}
color={isPublished ? colors.accent : colors.contentSecondary}
style={{ marginRight: 10 }}
/>
<ThemedText
style={[
styles.shareTitle,
isPublished && { color: colors.accent },
]}
>
<Trans>Share Workout</Trans>
</ThemedText>
</View>
{publishMutation.isPending || isPublishLoading ? (
<ActivityIndicator size="small" color={colors.accent} />
) : (
<View pointerEvents="none">
<Switch value={isPublished} color={colors.accent} />
</View>
)}
</TouchableOpacity>
{publishMutation.isError && (
<ThemedText style={[styles.shareError, { color: colors.danger }]}>
<Trans>Failed to update sharing. Please try again.</Trans>
</ThemedText>
)}
</>
)}
Comment thread
isotronic marked this conversation as resolved.
</ScrollView>
<View style={styles.startButtonContainer}>
<Button
Expand Down Expand Up @@ -348,5 +400,28 @@ function createStyles(colors: AppThemeColors) {
fontSize: 16,
paddingVertical: 4,
},
shareRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: colors.card,
borderRadius: radii.md,
padding: 14,
marginTop: 16,
},
shareLeft: {
flexDirection: "row",
alignItems: "center",
flex: 1,
},
shareTitle: {
fontSize: 15,
fontWeight: "600",
},
shareError: {
fontSize: 13,
paddingHorizontal: 16,
paddingBottom: 8,
},
});
}
15 changes: 15 additions & 0 deletions app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { useSettingsQuery } from "@/hooks/useSettingsQuery";
import { ThemedView } from "@/components/ThemedView";
import { t } from "@lingui/core/macro";
import { useAppTheme } from "@/theme";
import { useSocialListeners } from "../../hooks/useSocialListeners";

export default function AppLayout() {
const { colors } = useAppTheme();
const { data: settings, isLoading: settingsLoading } = useSettingsQuery();
useSocialListeners();

if (settingsLoading) {
return <ThemedView style={{ flex: 1 }}></ThemedView>;
Expand Down Expand Up @@ -43,6 +45,19 @@ export default function AppLayout() {
name="exercise-library"
options={{ title: t`Exercise Library` }}
/>
<Stack.Screen
name="friend-profile"
options={{ title: t`Friend Profile` }}
/>
<Stack.Screen name="friend-plan" options={{ title: t`Plan Details` }} />
<Stack.Screen
name="friend-workout"
options={{ title: t`Workout Details` }}
/>
<Stack.Screen
name="friend-exercise"
options={{ title: t`Exercise Details` }}
/>
<Stack.Screen name="+not-found" />
</Stack>
);
Expand Down
32 changes: 29 additions & 3 deletions app/(app)/custom-exercise.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useMemo } from "react";
import { useState, useEffect, useRef, useMemo, useContext } from "react";
import {
ScrollView,
TextInput,
Expand All @@ -14,6 +14,9 @@ import { ThemedText } from "@/components/ThemedText";
import * as ImagePicker from "expo-image-picker";
import * as FileSystem from "expo-file-system/legacy";
import { Exercise, fetchAllRecords, openDatabase } from "@/utils/database";
import { AuthContext } from "@/context/AuthProvider";
import { useSocialStore } from "@/store/socialStore";
import { pushCustomExercise } from "@/utils/sharing";
import { capitalizeWords } from "@/utils/utility";
import { ThemedView } from "@/components/ThemedView";
import { router, useLocalSearchParams, useNavigation } from "expo-router";
Expand All @@ -32,6 +35,8 @@ export default function AddCustomExerciseScreen() {

const queryClient = useQueryClient();
const navigation = useNavigation();
const user = useContext(AuthContext);
const { privacySettings } = useSocialStore();
const { setNewExerciseId } = useWorkoutStore();
const { exercise_id } = useLocalSearchParams();
const isEditing = !!exercise_id;
Expand Down Expand Up @@ -233,6 +238,8 @@ export default function AddCustomExerciseScreen() {
try {
const db = await openDatabase("userData.db");

let newExerciseResult: { exercise_id: number } | null = null;

if (isEditing) {
await db.runAsync(
`UPDATE exercises SET name = ?, description = ?, local_animated_uri = ?, body_part = ?, target_muscle = ?, equipment = ?, secondary_muscles = ?, is_unilateral = ?, double_weight = ? WHERE exercise_id = ?`,
Expand Down Expand Up @@ -266,10 +273,10 @@ export default function AddCustomExerciseScreen() {
],
);

const result = (await db.getFirstAsync(
newExerciseResult = (await db.getFirstAsync(
`SELECT exercise_id FROM exercises ORDER BY exercise_id DESC LIMIT 1`,
)) as { exercise_id: number };
setNewExerciseId(result?.exercise_id);
setNewExerciseId(newExerciseResult?.exercise_id);
}

queryClient.invalidateQueries({ queryKey: ["plan"] });
Expand All @@ -280,6 +287,25 @@ export default function AddCustomExerciseScreen() {
});
}
isDirtyRef.current = false;

if (user && privacySettings?.shareCustomExercises) {
const savedExerciseId = isEditing
? Number(exercise_id)
: newExerciseResult?.exercise_id;
if (savedExerciseId) {
const sharingDb = await openDatabase("userData.db");
const savedExercise = await sharingDb.getFirstAsync<Exercise>(
`SELECT * FROM exercises WHERE exercise_id = ?`,
[savedExerciseId],
);
if (savedExercise) {
pushCustomExercise(user.uid, savedExercise).catch((err) =>
Bugsnag.notify(err),
);
}
}
}

router.back();
} catch (error: any) {
Alert.alert(
Expand Down
Loading
Loading