Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"format:tailwind:check": "biome lint . --only=nursery/useSortedClasses",
"test": "vitest run",
"check": "run-s lint typecheck",
"check:compiler": "pnpm dlx react-compiler-healthcheck@latest",
"typecheck": "tsc --noEmit",
"check:unused": "knip"
},
Expand Down
32 changes: 17 additions & 15 deletions src/app/(app)/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,12 @@ function DragStartSlider({
.failOffsetY([-14 * scale, 14 * scale])
.onBegin(() => {
"worklet";
dragX.value = 0;
isCompleting.value = false;
dragX.set(0);
isCompleting.set(false);
})
.onUpdate((event) => {
"worklet";
dragX.value = Math.min(Math.max(event.translationX, 0), maxDrag);
dragX.set(Math.min(Math.max(event.translationX, 0), maxDrag));
})
.onEnd((event) => {
"worklet";
Expand All @@ -179,25 +179,27 @@ function DragStartSlider({
nextValue >= maxDrag * 0.72 ||
(event.velocityX > 650 && nextValue >= maxDrag * 0.35);

if (!shouldComplete || isCompleting.value) return;
if (!shouldComplete || isCompleting.get()) return;

isCompleting.value = true;
dragX.value = withTiming(maxDrag, { duration: 90 }, (finished) => {
"worklet";
if (!finished) return;
scheduleOnRN(onComplete);
dragX.value = withTiming(0, { duration: 120 });
isCompleting.value = false;
});
isCompleting.set(true);
dragX.set(
withTiming(maxDrag, { duration: 90 }, (finished) => {
"worklet";
if (!finished) return;
scheduleOnRN(onComplete);
dragX.set(withTiming(0, { duration: 120 }));
isCompleting.set(false);
}),
);
})
.onFinalize(() => {
"worklet";
if (isCompleting.value) return;
dragX.value = withTiming(0, { duration: 160 });
if (isCompleting.get()) return;
dragX.set(withTiming(0, { duration: 160 }));
});

const knobAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: dragX.value }],
transform: [{ translateX: dragX.get() }],
}));

return (
Expand Down
79 changes: 43 additions & 36 deletions src/app/notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const SWIPE_DELETE_MIN_DISTANCE = 96;
const SWIPE_DELETE_MAX_DISTANCE = 132;
const SWIPE_DELETE_FLING_DISTANCE = 40;
const SWIPE_DELETE_FLING_VELOCITY = -700;
const NOTIFICATION_EXIT = FadeOut.duration(90);
const NOTIFICATION_LAYOUT = LinearTransition.springify()
.damping(20)
.stiffness(180);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const getNotificationsModule = () => {
try {
Expand Down Expand Up @@ -171,8 +175,8 @@ function CategoryTabs({
const indicatorWidth = useSharedValue(0);
const selectionPosition = useSharedValue(selectedIndex);
const indicatorStyle = useAnimatedStyle(() => ({
width: indicatorWidth.value,
transform: [{ translateX: selectionPosition.value * indicatorWidth.value }],
width: indicatorWidth.get(),
transform: [{ translateX: selectionPosition.get() * indicatorWidth.get() }],
}));

useEffect(() => {
Expand Down Expand Up @@ -262,7 +266,7 @@ function CategoryTabLabel({
}) {
const animatedStyle = useAnimatedStyle(() => ({
color: interpolateColor(
Math.min(Math.abs(selectionPosition.value - index), 1),
Math.min(Math.abs(selectionPosition.get() - index), 1),
[0, 1],
["#FFFFFF", "#1A1A1A"],
),
Expand Down Expand Up @@ -306,20 +310,19 @@ function NotificationCard({
.failOffsetY([-12, 12])
.onUpdate((event) => {
"worklet";
if (isDeleting.value) return;
if (isDeleting.get()) return;

translateX.value = Math.max(
Math.min(event.translationX, 0),
-cardWidth.value,
translateX.set(
Math.max(Math.min(event.translationX, 0), -cardWidth.get()),
);
})
.onEnd((event) => {
"worklet";
if (isDeleting.value) return;
if (isDeleting.get()) return;

const distance = -translateX.value;
const distance = -translateX.get();
const deleteThreshold = Math.min(
Math.max(cardWidth.value * 0.3, SWIPE_DELETE_MIN_DISTANCE),
Math.max(cardWidth.get() * 0.3, SWIPE_DELETE_MIN_DISTANCE),
SWIPE_DELETE_MAX_DISTANCE,
);
const shouldDelete =
Expand All @@ -329,40 +332,44 @@ function NotificationCard({

if (!shouldDelete) return;

isDeleting.value = true;
translateX.value = withTiming(
-cardWidth.value - 24,
{
duration: 180,
easing: Easing.out(Easing.cubic),
},
(finished) => {
"worklet";
if (finished) scheduleOnRN(commitDelete);
},
isDeleting.set(true);
translateX.set(
withTiming(
-cardWidth.get() - 24,
{
duration: 180,
easing: Easing.out(Easing.cubic),
},
(finished) => {
"worklet";
if (finished) scheduleOnRN(commitDelete);
},
),
);
})
.onFinalize(() => {
"worklet";
if (isDeleting.value) return;

translateX.value = withSpring(0, {
damping: 20,
mass: 0.7,
overshootClamping: true,
stiffness: 260,
});
if (isDeleting.get()) return;

translateX.set(
withSpring(0, {
damping: 20,
mass: 0.7,
overshootClamping: true,
stiffness: 260,
}),
);
});

const cardAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
transform: [{ translateX: translateX.get() }],
}));
const deleteBackgroundAnimatedStyle = useAnimatedStyle(() => {
const deleteThreshold = Math.min(
Math.max(cardWidth.value * 0.3, SWIPE_DELETE_MIN_DISTANCE),
Math.max(cardWidth.get() * 0.3, SWIPE_DELETE_MIN_DISTANCE),
SWIPE_DELETE_MAX_DISTANCE,
);
const progress = Math.min(-translateX.value / deleteThreshold, 1);
const progress = Math.min(-translateX.get() / deleteThreshold, 1);

return {
backgroundColor: interpolateColor(
Expand All @@ -374,10 +381,10 @@ function NotificationCard({
});
const deleteIconAnimatedStyle = useAnimatedStyle(() => {
const deleteThreshold = Math.min(
Math.max(cardWidth.value * 0.3, SWIPE_DELETE_MIN_DISTANCE),
Math.max(cardWidth.get() * 0.3, SWIPE_DELETE_MIN_DISTANCE),
SWIPE_DELETE_MAX_DISTANCE,
);
const progress = Math.min(-translateX.value / deleteThreshold, 1);
const progress = Math.min(-translateX.get() / deleteThreshold, 1);

return {
opacity: interpolate(progress, [0, 0.18, 1], [0, 1, 1], "clamp"),
Expand All @@ -402,8 +409,8 @@ function NotificationCard({
return (
<Animated.View
className="my-1 rounded-[24px]"
exiting={FadeOut.duration(90)}
layout={LinearTransition.springify().damping(20).stiffness(180)}
exiting={NOTIFICATION_EXIT}
layout={NOTIFICATION_LAYOUT}
style={{ boxShadow: "0 8px 18px rgba(20, 28, 48, 0.08)" }}
>
<Animated.View
Expand Down
10 changes: 9 additions & 1 deletion src/components/Mascot.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type FC, useEffect, useState } from "react";
import { Animated, Easing, View } from "react-native";
import { useReducedMotion } from "react-native-reanimated";
import Svg, { Circle, Ellipse, G, Path, Rect } from "react-native-svg";

interface MascotProps {
Expand All @@ -18,8 +19,15 @@ interface MascotProps {
export const Mascot: FC<MascotProps> = ({ size = 120, pose = "default" }) => {
const [floatAnim] = useState(() => new Animated.Value(0));
const [moodAnim] = useState(() => new Animated.Value(0));
const reduceMotion = useReducedMotion();

useEffect(() => {
floatAnim.stopAnimation();
moodAnim.stopAnimation();
floatAnim.setValue(0);
moodAnim.setValue(0);
if (reduceMotion) return;

const floatLoop = Animated.loop(
Animated.sequence([
Animated.timing(floatAnim, {
Expand Down Expand Up @@ -60,7 +68,7 @@ export const Mascot: FC<MascotProps> = ({ size = 120, pose = "default" }) => {
floatLoop.stop();
moodLoop.stop();
};
}, [floatAnim, moodAnim, pose]);
}, [floatAnim, moodAnim, pose, reduceMotion]);

const translateY = floatAnim.interpolate({
inputRange: [0, 1],
Expand Down
43 changes: 25 additions & 18 deletions src/components/bottom-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,25 @@ function AnimatedTabIcon({
const focusProgress = useSharedValue(active ? 1 : 0);

useEffect(() => {
focusProgress.value = withSpring(active ? 1 : 0, {
damping: 14,
mass: 0.55,
stiffness: 260,
});
focusProgress.set(
withSpring(active ? 1 : 0, {
damping: 14,
mass: 0.55,
stiffness: 260,
}),
);
}, [active, focusProgress]);

const animatedStyle = useAnimatedStyle(() => ({
opacity: 0.76 + focusProgress.value * 0.24,
transform: [
{ translateY: focusProgress.value * -2 * scale },
{ scale: 1 + focusProgress.value * 0.1 },
],
}));
const animatedStyle = useAnimatedStyle(() => {
const progress = focusProgress.get();
return {
opacity: 0.76 + progress * 0.24,
transform: [
{ translateY: progress * -2 * scale },
{ scale: 1 + progress * 0.1 },
],
};
});

return (
<Animated.View style={animatedStyle}>
Expand All @@ -110,17 +115,19 @@ export function BottomNav({ state, navigation }: BottomNavProps) {
const indicatorPosition = useSharedValue(activeItemIndex);

useEffect(() => {
indicatorPosition.value = withSpring(activeItemIndex, {
damping: 17,
mass: 0.72,
stiffness: 210,
});
indicatorPosition.set(
withSpring(activeItemIndex, {
damping: 17,
mass: 0.72,
stiffness: 210,
}),
);
}, [activeItemIndex, indicatorPosition]);

const indicatorStyle = useAnimatedStyle(() => ({
transform: [
{
translateX: indicatorPosition.value * (ITEM_SIZE + ITEM_GAP) * scale,
translateX: indicatorPosition.get() * (ITEM_SIZE + ITEM_GAP) * scale,
},
],
}));
Expand Down
Loading