diff --git a/package-lock.json b/package-lock.json
index d80df45..451b826 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@expo/vector-icons": "^14.0.2",
"@noble/curves": "^1.6.0",
"@react-native-async-storage/async-storage": "^2.0.0",
+ "@react-native-community/slider": "^4.5.3",
"@react-navigation/native": "^6.0.2",
"@scure/base": "^1.1.8",
"@scure/bip39": "^1.4.0",
@@ -6384,6 +6385,12 @@
"node": ">=8"
}
},
+ "node_modules/@react-native-community/slider": {
+ "version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-4.5.3.tgz",
+ "integrity": "sha512-0mOdnIL3xPWYGkP7levDH3bBsjdMOiR5mQIzFrSkZznZdIfX9MvGnelNZxsWuibmkc3hczHybo7RRb6mE96H2g==",
+ "license": "MIT"
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.74.87",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.74.87.tgz",
diff --git a/package.json b/package.json
index ab277a4..45b0bf5 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@expo/vector-icons": "^14.0.2",
"@noble/curves": "^1.6.0",
"@react-native-async-storage/async-storage": "^2.0.0",
+ "@react-native-community/slider": "^4.5.3",
"@react-navigation/native": "^6.0.2",
"@scure/base": "^1.1.8",
"@scure/bip39": "^1.4.0",
diff --git a/src/app/(app)/_layout.tsx b/src/app/(app)/_layout.tsx
index 59b0de9..4a49382 100644
--- a/src/app/(app)/_layout.tsx
+++ b/src/app/(app)/_layout.tsx
@@ -3,7 +3,10 @@ import React from "react";
import { AccountProvider } from "@/src/account/context";
import { useAuthContext } from "@/src/auth/context";
-import { InProxyProvider } from "@/src/psiphon/mockContext";
+import {
+ InProxyActivityProvider,
+ InProxyProvider,
+} from "@/src/psiphon/mockContext";
export default function AppLayout() {
const { mnemonic, deviceNonce } = useAuthContext();
@@ -14,15 +17,17 @@ export default function AppLayout() {
}
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/src/app/(app)/index.tsx b/src/app/(app)/index.tsx
index 70c3819..d06e040 100644
--- a/src/app/(app)/index.tsx
+++ b/src/app/(app)/index.tsx
@@ -3,6 +3,7 @@ import { useWindowDimensions } from "react-native";
import { ConduitHeader } from "@/src/components/ConduitHeader";
import { ConduitOrbToggle } from "@/src/components/ConduitOrbToggle";
+import { ConduitSettings } from "@/src/components/ConduitSettings";
import { ConduitStatus } from "@/src/components/ConduitStatus";
import { SafeAreaView } from "@/src/components/SafeAreaView";
@@ -18,8 +19,9 @@ export default function HomeScreen() {
{/* Status takes up the rest of the vertical space */}
+
);
}
diff --git a/src/common/utils.ts b/src/common/utils.ts
index df30363..db5d9fa 100644
--- a/src/common/utils.ts
+++ b/src/common/utils.ts
@@ -30,3 +30,11 @@ export function niceBytes(bytes: number) {
return `${bytes.toFixed(1)} ${unit}`;
}
+
+export function bytesToMB(bytes: number) {
+ return bytes / 1024 / 1024;
+}
+
+export function MBToBytes(MB: number) {
+ return MB * 1024 * 1024;
+}
diff --git a/src/components/ConduitOrbToggle.tsx b/src/components/ConduitOrbToggle.tsx
index be887b0..d2e023d 100644
--- a/src/components/ConduitOrbToggle.tsx
+++ b/src/components/ConduitOrbToggle.tsx
@@ -27,13 +27,16 @@ import {
} from "react-native-reanimated";
//import { useInProxyContext } from "@/src/psiphon/context";
-import { useInProxyContext } from "@/src/psiphon/mockContext";
+import {
+ useInProxyActivityContext,
+ useInProxyContext,
+} from "@/src/psiphon/mockContext";
import { palette, sharedStyles as ss } from "@/src/styles";
export function ConduitOrbToggle({ size }: { size: number }) {
const { t } = useTranslation();
- const { toggleInProxy, isInProxyRunning, inProxyCurrentConnectedClients } =
- useInProxyContext();
+ const { toggleInProxy, isInProxyRunning } = useInProxyContext();
+ const { inProxyCurrentConnectedClients } = useInProxyActivityContext();
const radius = size / 4;
const centeringTransform = [
@@ -113,13 +116,11 @@ export function ConduitOrbToggle({ size }: { size: number }) {
if (isInProxyRunning()) {
if (inProxyCurrentConnectedClients === 0) {
if (animationState !== "announcing") {
- console.log("enter announcing state");
animateAnnouncing();
setAnimationState("announcing");
}
} else {
if (animationState !== "active") {
- console.log("enter peers connected state");
animatePeersConnected();
setAnimationState("active");
}
@@ -128,7 +129,6 @@ export function ConduitOrbToggle({ size }: { size: number }) {
} else {
randomizeVelocity.setActive(false);
if (animationState !== "idle") {
- console.log("enter idle state");
animateTurnOff();
setAnimationState("idle");
}
diff --git a/src/components/ConduitSettings.tsx b/src/components/ConduitSettings.tsx
new file mode 100644
index 0000000..7c610d0
--- /dev/null
+++ b/src/components/ConduitSettings.tsx
@@ -0,0 +1,328 @@
+import { FontAwesome, Ionicons, MaterialIcons } from "@expo/vector-icons";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Modal, Pressable, ScrollView, Text, View } from "react-native";
+
+import { useAccountContext } from "@/src/account/context";
+import { EditableNumberSlider } from "@/src/components/EditableNumberSlider";
+import { ProxyID } from "@/src/components/ProxyID";
+import { InProxyParametersSchema, getProxyId } from "@/src/psiphon/inproxy";
+import { useInProxyContext } from "@/src/psiphon/mockContext";
+import { lineItemStyle, palette, sharedStyles as ss } from "@/src/styles";
+import { handleError, wrapError } from "../common/errors";
+import { MBToBytes, bytesToMB } from "../common/utils";
+
+export function ConduitSettings() {
+ const { t } = useTranslation();
+ const { conduitKeyPair } = useAccountContext();
+ const {
+ inProxyParameters,
+ selectInProxyParameters,
+ isInProxyRunning,
+ sendFeedback,
+ } = useInProxyContext();
+
+ const [modalOpen, setModalOpen] = React.useState(false);
+ const [sendDiagnosticIcon, setSendDiagnosticIcon] = React.useState(
+ ,
+ );
+ const [displayTotalMaxMbps, setDisplayTotalMaxMbps] = React.useState(
+ bytesToMB(
+ inProxyParameters.limitUpstreamBytesPerSecond *
+ inProxyParameters.maxClients,
+ ),
+ );
+ const [displayRestartConfirmation, setDisplayRestartConfirmation] =
+ React.useState(false);
+
+ // TODO: better way to make a copy?
+ const [modifiedInProxyParameters, setModifiedInProxyParameters] =
+ React.useState(JSON.parse(JSON.stringify(inProxyParameters)));
+
+ async function updateInProxyMaxClients(newValue: number) {
+ modifiedInProxyParameters.maxClients = newValue;
+ setModifiedInProxyParameters(modifiedInProxyParameters);
+ setDisplayTotalMaxMbps(
+ bytesToMB(
+ newValue *
+ modifiedInProxyParameters.limitUpstreamBytesPerSecond,
+ ),
+ );
+ }
+
+ async function updateInProxyLimitBytesPerSecond(newValue: number) {
+ // This value is configured as MBps in UI, so multiply out to raw bytes
+ modifiedInProxyParameters.limitUpstreamBytesPerSecond =
+ MBToBytes(newValue);
+ modifiedInProxyParameters.limitDownstreamBytesPerSecond =
+ MBToBytes(newValue);
+ setModifiedInProxyParameters(modifiedInProxyParameters);
+ setDisplayTotalMaxMbps(newValue * modifiedInProxyParameters.maxClients);
+ }
+
+ async function commitChanges() {
+ const newInProxyParameters = InProxyParametersSchema.safeParse(
+ modifiedInProxyParameters,
+ );
+ if (newInProxyParameters.error) {
+ handleError(
+ wrapError(
+ newInProxyParameters.error,
+ "Error parsing updated InProxyParameters",
+ ),
+ );
+ return;
+ }
+ selectInProxyParameters(newInProxyParameters.data);
+ }
+
+ async function onSettingsClose() {
+ let settingsChanged = false;
+ if (
+ modifiedInProxyParameters.maxClients !==
+ inProxyParameters.maxClients
+ ) {
+ settingsChanged = true;
+ } else if (
+ modifiedInProxyParameters.limitUpstreamBytesPerSecond !==
+ inProxyParameters.limitUpstreamBytesPerSecond
+ ) {
+ settingsChanged = true;
+ }
+ if (settingsChanged) {
+ if (isInProxyRunning()) {
+ setDisplayRestartConfirmation(true);
+ } else {
+ await commitChanges();
+ setModalOpen(false);
+ }
+ } else {
+ setModalOpen(false);
+ }
+ }
+
+ function Settings() {
+ return (
+ <>
+
+
+ {t("EDIT_SETTINGS_I18N.string")}
+
+
+
+ {t("DONE_I18N.string")}
+
+
+
+
+
+
+
+
+
+ {t("MAX_BANDWIDTH_USAGE_I18N.string")}
+
+
+ {displayTotalMaxMbps} Mbps
+
+
+
+
+ {t("YOUR_ID_I18N.string")}
+
+
+
+
+
+ {t("SEND_DIAGNOSTIC_I18N.string")}
+
+ {
+ sendFeedback();
+ setSendDiagnosticIcon(
+ ,
+ );
+ setTimeout(() => {
+ setSendDiagnosticIcon(
+ ,
+ );
+ }, 3000);
+ }}
+ >
+ {sendDiagnosticIcon}
+
+
+
+
+ >
+ );
+ }
+
+ function RestartConfirmation() {
+ return (
+
+
+
+ {t(
+ "SETTINGS_CHANGE_WILL_RESTART_CONDUIT_DESCRIPTION_I18N.string",
+ )}
+
+
+ {
+ await commitChanges();
+ setModalOpen(false);
+ setDisplayRestartConfirmation(false);
+ }}
+ >
+
+ {t("CONFIRM_I18N.string")}
+
+
+ {
+ setDisplayRestartConfirmation(false);
+ setModalOpen(false);
+ }}
+ >
+
+ {t("CANCEL_I18N.string")}
+
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+ {
+ setModalOpen(true);
+ }}
+ >
+
+
+
+ {/* this empty modal fades in the opacity overlay */}
+
+
+
+ {/* this modal has the settings menu and slides up */}
+
+
+ {displayRestartConfirmation ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/components/ConduitStatus.tsx b/src/components/ConduitStatus.tsx
index d0dde44..eff577f 100644
--- a/src/components/ConduitStatus.tsx
+++ b/src/components/ConduitStatus.tsx
@@ -12,12 +12,15 @@ import {
import React from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
+import { useSharedValue, withTiming } from "react-native-reanimated";
import { pathFromPoints } from "@/src/common/skia";
import { niceBytes } from "@/src/common/utils";
-import { useInProxyContext } from "@/src/psiphon/mockContext";
+import {
+ useInProxyActivityContext,
+ useInProxyContext,
+} from "@/src/psiphon/mockContext";
import { palette, sharedStyles as ss } from "@/src/styles";
-import { useSharedValue, withTiming } from "react-native-reanimated";
export function ConduitStatus({
width,
@@ -31,8 +34,8 @@ export function ConduitStatus({
inProxyActivityBy1000ms,
inProxyCurrentConnectedClients,
inProxyTotalBytesTransferred,
- isInProxyRunning,
- } = useInProxyContext();
+ } = useInProxyActivityContext();
+ const { isInProxyRunning } = useInProxyContext();
const connectedPeersText = t("CONNECTED_PEERS_I18N.string", {
peers: inProxyCurrentConnectedClients,
diff --git a/src/components/ConduitToggle.tsx b/src/components/ConduitToggle.tsx
new file mode 100644
index 0000000..09ab80d
--- /dev/null
+++ b/src/components/ConduitToggle.tsx
@@ -0,0 +1,331 @@
+import {
+ Blur,
+ Canvas,
+ Circle,
+ ColorMatrix,
+ ColorShader,
+ Group,
+ Paint,
+ RadialGradient,
+ Shadow,
+ Text,
+ interpolateColors,
+ interpolateVector,
+ useFont,
+ vec,
+} from "@shopify/react-native-skia";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Pressable, View } from "react-native";
+import {
+ useDerivedValue,
+ useFrameCallback,
+ useSharedValue,
+ withClamp,
+ withRepeat,
+ withSequence,
+ withTiming,
+} from "react-native-reanimated";
+
+import { palette, sharedStyles as ss } from "@/src/styles";
+
+export function ConduitToggle({ size }: { size: number }) {
+ const { t } = useTranslation();
+
+ // The button is placed into a square Canvas that is 1/2.5 x the size, so that
+ // there is room for the orbiting elements. Therefore, our transform needs
+ // to center at 3/2 radius.
+ const radius = size / 4;
+ const centeringTransform = [
+ {
+ translateY: size / 2,
+ },
+ {
+ translateX: size / 2,
+ },
+ ];
+ const [inProxyOn, setInProxyOn] = React.useState(false);
+ const [inProxyPeersConnected, setInProxyPeersConnected] = React.useState(0);
+ const buttonText = t("TURN_ON_I18N.string");
+
+ const dxA = useSharedValue(10);
+ const dxB = useSharedValue(-10);
+ const animatedBlur = useSharedValue(10);
+ const buttonInnerColours = [
+ palette.redShade3,
+ palette.redShade2,
+ palette.redShade1,
+ palette.red,
+ palette.blue,
+ ];
+ const buttonOuterColours = [
+ palette.purpleShade5,
+ palette.purpleShade3,
+ palette.purpleShade1,
+ palette.purple,
+ palette.purpleShade3,
+ ];
+ const buttonColoursIndex = useSharedValue(0);
+ const buttonTextColours = [palette.redTint3, palette.transparent];
+ const buttonTextColourIndex = useSharedValue(0);
+ const growRadius = useSharedValue(0);
+ const spinner = useSharedValue(0);
+
+ function animateAnnouncing() {
+ dxA.value = withTiming(0, { duration: 2000 });
+ dxB.value = withTiming(0, { duration: 2000 });
+ animatedBlur.value = withTiming(0, { duration: 2000 });
+ growRadius.value = withTiming(radius, { duration: 500 });
+ buttonColoursIndex.value = withRepeat(
+ // only animate through the first 4 colors
+ withTiming(3, {
+ duration: 2000,
+ }),
+ -1,
+ true,
+ );
+ buttonTextColourIndex.value = withTiming(1, { duration: 500 });
+ }
+
+ function animatePeersConnected() {
+ colorsIndex.value = withTiming(4, {duration: 2000});
+ }
+
+ function animateTurnOff() {
+ dxA.value = withTiming(10, { duration: 2000 });
+ dxB.value = withTiming(-10, { duration: 2000 });
+ animatedBlur.value = withTiming(10, { duration: 2000 });
+ growRadius.value = withTiming(0, { duration: 2000 });
+ buttonColoursIndex.value = withTiming(0, { duration: 500 });
+ buttonTextColourIndex.value = withTiming(0, { duration: 500 });
+ spinner.value = withTiming(0, { duration: 500 });
+ }
+
+ React.useEffect(() => {
+ if (inProxyOn) {
+ if (inProxyPeersConnected === 0) {
+ animateAnnouncing();
+ } else {
+ animatePeersConnected();
+ randomizeVelocity.setActive(true);
+ }
+ } else {
+ animateTurnOff();
+ randomizeVelocity.setActive(false);
+ }
+ }, [inProxyOn, inProxyPeersConnected]);
+
+ const buttonGradientColours = useDerivedValue(() => {
+ return [
+ interpolateColors(colorsIndex.value, [0, 1, 2, 3, 4, 5], startColors),
+ interpolateColors(colorsIndex.value, [0, 1, 2, 3, 4, 5], endColors),
+ ];
+ });
+
+ const buttonTextColour = useDerivedValue(() => {
+ return interpolateColors(
+ buttonTextColourIndex.value,
+ [0, 1],
+ buttonTextColours,
+ );
+ });
+
+ // will use these 2 random values to randomize the Y values that the orbs
+ // take as they traverse the conduit orb.
+ const randomA = useSharedValue(1);
+ const randomB = useSharedValue(1);
+ const startSign = useSharedValue(1);
+ const endSign = useSharedValue(1);
+ const randomizerReady = useSharedValue(0);
+ const randomizeVelocity = useFrameCallback((_) => {
+ // pick new random values after each loop of the spinner
+ if (spinner.value > 0.9 && randomizerReady.value === 1) {
+ randomA.value = Math.random();
+ randomB.value = Math.random();
+ if (Math.random() > 0.5) {
+ startSign.value = 1;
+ endSign.value = 1;
+ } else {
+ startSign.value = -1;
+ endSign.value = -1;
+ }
+ randomizerReady.value = 0;
+ console.log("randomized at end");
+ }
+ // reset the randomizer at the start of each loop
+ if (spinner.value < 0.1 && randomizerReady.value === 0) {
+ randomA.value = Math.random();
+ randomB.value = Math.random();
+ if (Math.random() > 0.5) {
+ startSign.value = 1;
+ endSign.value = 1;
+ } else {
+ startSign.value = -1;
+ endSign.value = -1;
+ }
+ randomizerReady.value = 1;
+ console.log("randomized at start");
+ }
+ });
+
+ const spinVec = useDerivedValue(() => {
+ return interpolateVector(
+ spinner.value,
+ [-1, -0.6, 0, 0.6, 1],
+ [
+ vec(-size, startSign.value * size * randomA.value),
+ vec(-radius, startSign.value * radius * randomA.value),
+ vec(0, 0),
+ vec(radius, endSign.value * radius * randomB.value),
+ vec(size, endSign.value * size * randomB.value),
+ ],
+ );
+ });
+
+ // START TODO: Placeholder
+ const mockPeersRef = React.useRef | null>(
+ null,
+ );
+ const toggleInProxy = () => {
+ console.log("toggle in proxy");
+ if (inProxyPeersConnected !== 0) {
+ setInProxyPeersConnected(0);
+ }
+ if (!inProxyOn) {
+ setInProxyOn(true);
+ if (!mockPeersRef.current) {
+ mockPeersRef.current = setTimeout(() => {
+ setInProxyPeersConnected(5);
+ }, 5000);
+ }
+ } else {
+ setInProxyOn(false);
+ if (mockPeersRef.current) {
+ clearTimeout(mockPeersRef.current);
+ mockPeersRef.current = null;
+ }
+ }
+ };
+ // END TODO: Placeholder
+
+ // Inspired by the "Metaball Animation" tutorial in react-native-skia docs
+ const morphLayer = React.useMemo(() => {
+ return (
+
+
+
+
+ );
+ }, []);
+
+ const font = useFont(
+ require("../../assets/fonts/SpaceMono-Regular.ttf"),
+ 20,
+ );
+ if (!font) {
+ return null;
+ }
+ const buttonTextXOffset = -font.measureText(buttonText).width / 2;
+ const buttonTextYOffset = font.measureText(buttonText).height / 2;
+
+ return (
+
+
+ {
+ toggleInProxy();
+ }}
+ />
+
+ );
+}
diff --git a/src/components/EditableNumberSlider.tsx b/src/components/EditableNumberSlider.tsx
new file mode 100644
index 0000000..ff9b713
--- /dev/null
+++ b/src/components/EditableNumberSlider.tsx
@@ -0,0 +1,129 @@
+import { MaterialIcons } from "@expo/vector-icons";
+import Slider from "@react-native-community/slider";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Pressable, Text, View } from "react-native";
+
+import { lineItemStyle, palette, sharedStyles as ss } from "@/src/styles";
+
+export function EditableNumberSlider({
+ label,
+ originalValue,
+ min,
+ max,
+ step,
+ units = "",
+ style = lineItemStyle,
+ onCommit,
+}: {
+ label: string;
+ originalValue: number;
+ min: number;
+ max: number;
+ step: number;
+ units?: string;
+ style?: any;
+ onCommit: (newValue: number) => Promise;
+}) {
+ const { t } = useTranslation();
+ const [value, setValue] = React.useState(originalValue);
+ const [isEditing, setIsEditing] = React.useState(false);
+
+ async function commit() {
+ if (value === originalValue) {
+ setIsEditing(false);
+ } else {
+ await onCommit(value);
+ setIsEditing(false);
+ }
+ }
+
+ if (!isEditing) {
+ return (
+
+ {label}
+
+
+
+
+ {value}
+
+
+ {units}
+
+ setIsEditing(true)}
+ >
+
+
+
+
+ );
+ } else {
+ return (
+
+ setValue(value)}
+ maximumTrackTintColor="white"
+ minimumTrackTintColor={palette.redTint2}
+ thumbTintColor={palette.redTint2}
+ />
+
+
+
+
+ {value}
+
+ {value === originalValue ? "" : "*"}
+
+
+
+ {units}
+
+
+
+
+ {t("SAVE_I18N.string")}
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/components/ProxyID.tsx b/src/components/ProxyID.tsx
index f3caa4d..d798f90 100644
--- a/src/components/ProxyID.tsx
+++ b/src/components/ProxyID.tsx
@@ -40,7 +40,7 @@ export function ProxyID({
ss.rounded5,
ss.halfPadded,
{
- backgroundColor: palette.blue,
+ backgroundColor: palette.redTint2,
},
]}
>
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json
index cfd7bf2..f4aa6af 100644
--- a/src/i18n/locales/en/translation.json
+++ b/src/i18n/locales/en/translation.json
@@ -12,7 +12,7 @@
"developer_comment": "Indicates to the user that Conduit is off. The name 'Conduit' is the app name and should not be translated."
},
"YOUR_ID_I18N": {
- "string": "Your Conduit ID:",
+ "string": "Your Conduit ID",
"developer_comment": "Text displayed to the user to indicate their Conduit ID. The name 'Conduit' is the app name and should not be translated."
},
"ANNOUNCING_I18N": {
@@ -32,5 +32,17 @@
},
"CONNECTED_PEERS_I18N": {
"string": "{{ peers }} connected peers"
+ },
+ "DONE_I18N": {
+ "string": "Done"
+ },
+ "CANCEL_I18N": {
+ "string": "Cancel"
+ },
+ "CONFIRM_I18N": {
+ "string": "Confirm"
+ },
+ "SETTINGS_CHANGE_WILL_RESTART_CONDUIT_DESCRIPTION_I18N": {
+ "string": "Changing Conduit settings requires restarting your Conduit Station. Any connected users will be disconnected. Confirm changes?"
}
}
diff --git a/src/psiphon/inproxy.ts b/src/psiphon/inproxy.ts
index b4ac9c3..6344295 100644
--- a/src/psiphon/inproxy.ts
+++ b/src/psiphon/inproxy.ts
@@ -2,8 +2,17 @@ import { edwardsToMontgomeryPub } from "@noble/curves/ed25519";
import { base64nopad } from "@scure/base";
import { z } from "zod";
-import { Ed25519KeyPair } from "@/src/common/cryptography";
+import {
+ Ed25519KeyPair,
+ generateEd25519KeyPair,
+ keyPairToBase64nopad,
+} from "@/src/common/cryptography";
import { Base64Unpadded64Bytes } from "@/src/common/validators";
+import { wrapError } from "../common/errors";
+import {
+ DEFAULT_INPROXY_LIMIT_BYTES_PER_SECOND,
+ DEFAULT_INPROXY_MAX_CLIENTS,
+} from "../constants";
export const InProxyActivityDataByPeriodSchema = z.object({
bytesUp: z.array(z.number()).length(288),
@@ -23,22 +32,6 @@ export const InProxyActivityStatsSchema = z.object({
}),
});
-export const zeroedInProxyActivityStats = InProxyActivityStatsSchema.parse({
- elapsedTime: 0,
- totalBytesUp: 0,
- totalBytesDown: 0,
- currentConnectingClients: 0,
- currentConnectedClients: 0,
- dataByPeriod: {
- "1000ms": {
- bytesUp: new Array(288).fill(0),
- bytesDown: new Array(288).fill(0),
- connectedClients: new Array(288).fill(0),
- connectingClients: new Array(288).fill(0),
- },
- },
-});
-
// These are the user-configurable parameters for the inproxy.
export const InProxyParametersSchema = z.object({
privateKey: Base64Unpadded64Bytes,
@@ -59,6 +52,40 @@ export type InProxyActivityByPeriod = z.infer<
>;
export type InProxyError = z.infer;
+export function getDefaultInProxyParameters(): InProxyParameters {
+ const ephemeralKey = generateEd25519KeyPair();
+ if (ephemeralKey instanceof Error) {
+ throw wrapError(
+ ephemeralKey,
+ "Failed to get default InProxyParameters",
+ );
+ }
+ return InProxyParametersSchema.parse({
+ privateKey: keyPairToBase64nopad(ephemeralKey),
+ maxClients: DEFAULT_INPROXY_MAX_CLIENTS,
+ limitUpstreamBytesPerSecond: DEFAULT_INPROXY_LIMIT_BYTES_PER_SECOND,
+ limitDownstreamBytesPerSecond: DEFAULT_INPROXY_LIMIT_BYTES_PER_SECOND,
+ });
+}
+
+export function getZeroedInProxyActivityStats(): InProxyActivityStats {
+ return InProxyActivityStatsSchema.parse({
+ elapsedTime: 0,
+ totalBytesUp: 0,
+ totalBytesDown: 0,
+ currentConnectingClients: 0,
+ currentConnectedClients: 0,
+ dataByPeriod: {
+ "1000ms": {
+ bytesUp: new Array(288).fill(0),
+ bytesDown: new Array(288).fill(0),
+ connectedClients: new Array(288).fill(0),
+ connectingClients: new Array(288).fill(0),
+ },
+ },
+ });
+}
+
/** This is used to derive the conduit key pair from the mnemonic. The chosen
* path is not that important, but each device should have it's own unique
* conduit key pair, so we use the device nonce as the last index. The root
diff --git a/src/psiphon/mockContext.tsx b/src/psiphon/mockContext.tsx
index 7979310..a361aa4 100644
--- a/src/psiphon/mockContext.tsx
+++ b/src/psiphon/mockContext.tsx
@@ -4,7 +4,8 @@ import {
InProxyActivityByPeriod,
InProxyActivityStats,
InProxyParameters,
- zeroedInProxyActivityStats,
+ getDefaultInProxyParameters,
+ getZeroedInProxyActivityStats,
} from "@/src/psiphon/inproxy";
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
@@ -13,7 +14,7 @@ async function* generateMockData(
): AsyncGenerator {
// initial empty data, representing no usage
// TODO: this is a crappy way to clone
- const data = JSON.parse(JSON.stringify(zeroedInProxyActivityStats));
+ const data = getZeroedInProxyActivityStats();
async function doTick() {
// shift every array to drop the first value
@@ -21,7 +22,7 @@ async function* generateMockData(
data.dataByPeriod["1000ms"].bytesUp.shift();
data.dataByPeriod["1000ms"].bytesDown.shift();
// 50% chance to add a user every tick, up to max
- if (Math.random() > 0.5 && data.currentConnectedClients <= maxClients) {
+ if (Math.random() > 0.5 && data.currentConnectedClients < maxClients) {
data.currentConnectedClients++;
data.dataByPeriod["1000ms"].connectedClients.push(1);
} else {
@@ -55,49 +56,41 @@ async function* generateMockData(
}
}
-export interface InProxyContextValue {
- inProxyParameters: InProxyParameters | null;
+export interface InProxyActivityContextValue {
inProxyActivityBy1000ms: InProxyActivityByPeriod;
inProxyCurrentConnectedClients: number;
inProxyTotalBytesTransferred: number;
- inProxyMustUpgrade: boolean;
- toggleInProxy: () => Promise;
- selectInProxyParameters: (params: InProxyParameters) => Promise;
- isInProxyRunning: () => boolean;
- sendFeedback: () => Promise;
}
-export const InProxyContext = React.createContext(
- null,
-);
+export const InProxyActivityContext =
+ React.createContext(null);
-export function useInProxyContext(): InProxyContextValue {
- const value = React.useContext(InProxyContext);
+export function useInProxyActivityContext(): InProxyActivityContextValue {
+ const value = React.useContext(InProxyActivityContext);
if (!value) {
throw new Error(
- "useInProxyContext must be used within a InProxyProvider",
+ "useInProxyActivityContext must be used within a InProxyActivityProvider",
);
}
return value;
}
-export function InProxyProvider({ children }: { children: React.ReactNode }) {
- const [inProxyRunning, setInProxyRunning] = React.useState(false);
- const [inProxyParameters, setInProxyParameters] =
- React.useState(null);
+export function InProxyActivityProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
const [inProxyActivityBy1000ms, setInProxyActivityBy1000ms] =
React.useState(
- zeroedInProxyActivityStats.dataByPeriod["1000ms"],
+ getZeroedInProxyActivityStats().dataByPeriod["1000ms"],
);
const [inProxyCurrentConnectedClients, setInProxyCurrentConnectedClients] =
React.useState(0);
const [inProxyTotalBytesTransferred, setInProxyTotalBytesTransferred] =
React.useState(0);
- // TODO: how to test this with the mock?
- //const [inProxyMustUpgrade, setInProxyMustUpgrade] =
- // React.useState(false);
- const inProxyMustUpgrade = false;
+
+ const { inProxyParameters, isInProxyRunning } = useInProxyContext();
function handleInProxyActivityStats(
inProxyActivityStats: InProxyActivityStats,
@@ -112,6 +105,86 @@ export function InProxyProvider({ children }: { children: React.ReactNode }) {
setInProxyActivityBy1000ms(inProxyActivityStats.dataByPeriod["1000ms"]);
}
+ // mock stats emitter
+ const mockDataGenerator = React.useRef(null);
+ React.useEffect(() => {
+ async function runMock() {
+ console.log("MOCK: Initializing mock data generation");
+ mockDataGenerator.current = generateMockData(
+ inProxyParameters!.maxClients,
+ );
+ let data = (await mockDataGenerator.current.next()).value;
+ while (data) {
+ if (data) {
+ handleInProxyActivityStats(data);
+ }
+ data = (await mockDataGenerator.current.next()).value;
+ }
+ handleInProxyActivityStats(getZeroedInProxyActivityStats());
+ }
+
+ async function stopMock() {
+ if (mockDataGenerator.current) {
+ console.log("MOCK: Stopping mock data generation");
+ await mockDataGenerator.current.return(
+ getZeroedInProxyActivityStats(),
+ );
+ }
+ }
+
+ if (isInProxyRunning()) {
+ runMock();
+ } else {
+ stopMock();
+ }
+ }, [inProxyParameters, isInProxyRunning]);
+
+ const value = {
+ inProxyActivityBy1000ms,
+ inProxyCurrentConnectedClients,
+ inProxyTotalBytesTransferred,
+ };
+ return (
+
+ {children}
+
+ );
+}
+
+export interface InProxyContextValue {
+ inProxyParameters: InProxyParameters;
+ inProxyMustUpgrade: boolean;
+ toggleInProxy: () => Promise;
+ selectInProxyParameters: (params: InProxyParameters) => Promise;
+ isInProxyRunning: () => boolean;
+ sendFeedback: () => Promise;
+}
+
+export const InProxyContext = React.createContext(
+ null,
+);
+
+export function useInProxyContext(): InProxyContextValue {
+ const value = React.useContext(InProxyContext);
+ if (!value) {
+ throw new Error(
+ "useInProxyContext must be used within a InProxyProvider",
+ );
+ }
+
+ return value;
+}
+
+export function InProxyProvider({ children }: { children: React.ReactNode }) {
+ const [inProxyRunning, setInProxyRunning] = React.useState(false);
+ const [inProxyParameters, setInProxyParameters] =
+ React.useState(getDefaultInProxyParameters());
+
+ // TODO: how to test this with the mock?
+ //const [inProxyMustUpgrade, setInProxyMustUpgrade] =
+ // React.useState(false);
+ const inProxyMustUpgrade = false;
+
//function handleInProxyError(inProxyError: InProxyError): void {
// console.log("MOCK: Received InProxy error", inProxyError);
// if (inProxyError.action === "inProxyMustUpgrade") {
@@ -124,6 +197,7 @@ export function InProxyProvider({ children }: { children: React.ReactNode }) {
params: InProxyParameters,
): Promise {
setInProxyParameters(params);
+
console.log("MOCK: InProxy parameters selected successfully");
}
@@ -131,7 +205,6 @@ export function InProxyProvider({ children }: { children: React.ReactNode }) {
//await requestNotificationsPermissions();
setInProxyRunning(!inProxyRunning);
console.log("MOCK: InProxyModule.toggleInProxy() invoked");
- setInProxyCurrentConnectedClients(0);
}
const isInProxyRunning = React.useCallback(() => {
@@ -142,48 +215,8 @@ export function InProxyProvider({ children }: { children: React.ReactNode }) {
console.log("MOCK: InProxyModule.sendFeedback() invoked");
}
- // mock stats emitter
- // DO THIS A SMARTER WAY
- const mockDataGenerator = React.useRef(null);
- React.useEffect(() => {
- if (inProxyParameters) {
- async function runMock() {
- console.log("MOCK: Initializing mock data generation");
- mockDataGenerator.current = generateMockData(
- inProxyParameters!.maxClients,
- );
- let data = (await mockDataGenerator.current.next()).value;
- while (data) {
- if (data) {
- handleInProxyActivityStats(data);
- }
- data = (await mockDataGenerator.current.next()).value;
- }
- handleInProxyActivityStats(zeroedInProxyActivityStats);
- }
-
- async function stopMock() {
- if (mockDataGenerator.current) {
- console.log("MOCK: Stopping mock data generation");
- await mockDataGenerator.current.return(
- zeroedInProxyActivityStats,
- );
- }
- }
-
- if (inProxyRunning) {
- runMock();
- } else {
- stopMock();
- }
- }
- }, [inProxyParameters, inProxyRunning]);
-
const value = {
inProxyParameters,
- inProxyActivityBy1000ms,
- inProxyCurrentConnectedClients,
- inProxyTotalBytesTransferred,
inProxyMustUpgrade,
toggleInProxy,
selectInProxyParameters,
diff --git a/src/styles.ts b/src/styles.ts
index ba275be..9d6cd5d 100644
--- a/src/styles.ts
+++ b/src/styles.ts
@@ -39,7 +39,7 @@ export const palette = {
grey: "#342F2F",
transparent: "rgba(0,0,0,0)",
transparentBlue: "rgba(59,122,150,0.5)",
- transparentPurple: "rgba(93,66,100,0.5)",
+ transparentPurple: "rgba(93,66,100,0.4)",
};
export const sharedStyles = StyleSheet.create({
@@ -243,6 +243,14 @@ export const sharedStyles = StyleSheet.create({
borderLeftWidth: 1,
borderColor: palette.white,
},
+ whiteBorderBottom: {
+ borderBottomWidth: 1,
+ borderColor: palette.white,
+ },
+ greyBorderBottom: {
+ borderBottomWidth: 1,
+ borderColor: palette.grey,
+ },
blackBg: {
backgroundColor: palette.black,
},
@@ -265,10 +273,8 @@ export const sharedStyles = StyleSheet.create({
flex: 1,
height: "50%",
width: "100%",
- backgroundColor: palette.grey,
- borderTopRightRadius: 40,
- borderTopLeftRadius: 40,
position: "absolute",
+ backgroundColor: palette.black,
padding: 20,
bottom: 0,
},
@@ -345,9 +351,16 @@ export const sharedStyles = StyleSheet.create({
position: "absolute",
left: 0,
top: 0,
- opacity: 0.5,
+ opacity: 0.7,
height: "100%",
width: "100%",
backgroundColor: "black",
},
});
+
+export const lineItemStyle = [
+ sharedStyles.padded,
+ sharedStyles.row,
+ sharedStyles.height60,
+ sharedStyles.greyBorderBottom,
+];