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
105 changes: 58 additions & 47 deletions components/Spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,95 @@ import Svg, { Circle } from "react-native-svg";
import Animated, {
Easing,
useAnimatedProps,
useAnimatedStyle,
useSharedValue,
withDelay,
withRepeat,
withSequence,
withTiming,
} from "react-native-reanimated";
import { useThemeColors } from "utils/theme";
import { testingEnvironment } from "helper/launchArguments";

const AnimatedCircle = Animated.createAnimatedComponent(Circle);

const SIZE = 64;
const DURATION = 1800;
const RADIUS_EASING = Easing.bezier(0.165, 0.84, 0.44, 1);
const OPACITY_EASING = Easing.bezier(0.3, 0.61, 0.355, 1);
const SIZE = 48;
const EASING = Easing.bezier(0.42, 0, 0.58, 1);
// dash cycle: 1.5s in three keyframes (grow, travel, hold), rotation 2s linear
const GROW = 712;
const HOLD = 76;

function PuffCircle({ delay, color }: { delay: number; color: string }) {
const radius = useSharedValue(1);
const opacity = useSharedValue(1);
// "ring-resize" by Utkarsh Verma (svg-spinners, MIT): arc grows while
// traveling around a ring. Static under Detox so the idle-sync doesn't hang.
export default function Spinner() {
const colors = useThemeColors();
const dash = useSharedValue(0);
const offset = useSharedValue(0);
const rotation = useSharedValue(0);

useEffect(() => {
radius.value = withDelay(
delay,
withRepeat(
withTiming(20, { duration: DURATION, easing: RADIUS_EASING }),
-1,
if (testingEnvironment()) return;
dash.value = withRepeat(
withSequence(
withTiming(42, { duration: GROW, easing: EASING }),
withTiming(42, { duration: GROW + HOLD }),
),
-1,
);
opacity.value = withDelay(
delay,
withRepeat(
withTiming(0, { duration: DURATION, easing: OPACITY_EASING }),
-1,
offset.value = withRepeat(
withSequence(
withTiming(-16, { duration: GROW, easing: EASING }),
withTiming(-59, { duration: GROW, easing: EASING }),
withTiming(-59, { duration: HOLD }),
),
-1,
);
}, [radius, opacity, delay]);
rotation.value = withRepeat(
withTiming(360, { duration: 2000, easing: Easing.linear }),
-1,
);
}, [dash, offset, rotation]);

const animatedProps = useAnimatedProps(() => ({
r: radius.value,
strokeOpacity: opacity.value,
strokeDasharray: [dash.value, 150],
strokeDashoffset: offset.value,
}));

return (
<AnimatedCircle
cx={22}
cy={22}
fill="none"
stroke={color}
strokeWidth={2}
animatedProps={animatedProps}
/>
);
}

// "puff" loader by Sam Herbert (SVG-Loaders, MIT): two expanding, fading
// rings. Static under Detox so the idle-sync doesn't hang.
export default function Spinner() {
const colors = useThemeColors();
const rotationStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }],
}));

if (testingEnvironment()) {
return (
<Svg width={SIZE} height={SIZE} viewBox="0 0 44 44">
<Svg width={SIZE} height={SIZE} viewBox="0 0 24 24">
<Circle
cx={22}
cy={22}
r={10}
cx={12}
cy={12}
r={9.5}
fill="none"
stroke={colors.primary}
strokeWidth={2}
strokeOpacity={0.5}
strokeWidth={2.5}
strokeLinecap="round"
strokeDasharray={[42, 150]}
strokeDashoffset={-16}
/>
</Svg>
);
}

return (
<Svg width={SIZE} height={SIZE} viewBox="0 0 44 44">
<PuffCircle delay={0} color={colors.primary} />
<PuffCircle delay={DURATION / 2} color={colors.primary} />
</Svg>
<Animated.View style={rotationStyle}>
<Svg width={SIZE} height={SIZE} viewBox="0 0 24 24">
<AnimatedCircle
cx={12}
cy={12}
r={9.5}
fill="none"
stroke={colors.primary}
strokeWidth={2.5}
strokeLinecap="round"
animatedProps={animatedProps}
/>
</Svg>
</Animated.View>
);
}
3 changes: 3 additions & 0 deletions screens/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export default function MainScreen({
return () => subscription.remove();
}, [navigation]);

// Switching servers starts disconnected; the new WebView reports back
useEffect(() => setIsConnected(false), [activeServer?.url]);

// Reconnect if connection is lost
useEffect(() => {
let intervalId: NodeJS.Timeout | undefined;
Expand Down
2 changes: 0 additions & 2 deletions screens/SwitchServerScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
} from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
import { TouchableOpacity, View } from "react-native";
import { delay } from "utils/delay";
import { APP_VERSION, GITHUB_RELEASES_URL } from "../utils/constants";
import Header from "components/Header";
import AppText from "components/AppText";
Expand All @@ -29,7 +28,6 @@ export default function SwitchServerScreen({
async (server: Server) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
await setActiveServer(server);
await delay(500);
navigation.goBack();
},
[setActiveServer, navigation],
Expand Down
2 changes: 0 additions & 2 deletions utils/delay.ts

This file was deleted.

Loading