diff --git a/.gitignore b/.gitignore
index 6623142..5b1f309 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,7 @@ web-build/
# The following patterns were generated by expo-cli
expo-env.d.ts
-# @end expo-cli
\ No newline at end of file
+# @end expo-cli
+
+# git hash version file
+src/git-hash.ts
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..98fd50c 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,10 @@
"main": "src/entrypoint",
"version": "1.0.0",
"scripts": {
- "android": "expo run:android",
- "build-release": "cd android && ./gradlew assembleRelease && cd ..",
- "ios": "expo run:ios",
+ "android": "npm run get-git-hash && expo run:android",
+ "ios": "npm run get-git-hash && expo run:ios",
+ "get-git-hash": "echo \"export const GIT_HASH = '$(git rev-parse --short HEAD)$(git status --porcelain | grep -q . && echo '-uncommitted')';\" > ./src/git-hash.ts",
+ "build-release": "npm run get-git-hash && cd android && ./gradlew assembleRelease && cd ..",
"test": "jest --verbose",
"format": "prettier --write ./src",
"tsc": "tsc --noUnusedLocals --noUnusedParameters",
@@ -27,6 +28,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/ConduitHeader.tsx b/src/components/ConduitHeader.tsx
index dd32c3c..81309a5 100644
--- a/src/components/ConduitHeader.tsx
+++ b/src/components/ConduitHeader.tsx
@@ -1,8 +1,10 @@
-import { View } from "react-native";
+import { Text, View } from "react-native";
+import { ConduitFlowerIcon } from "@/src/components/svgs/ConduitFlowerIcon";
+import { ConduitWordmark } from "@/src/components/svgs/ConduitWordmark";
import { palette, sharedStyles as ss } from "@/src/styles";
-import { ConduitFlowerIcon } from "./svgs/ConduitFlowerIcon";
-import { ConduitWordmark } from "./svgs/ConduitWordmark";
+// @ts-ignore
+import { GIT_HASH } from "@/src/git-hash";
export function ConduitHeader({
width,
@@ -20,9 +22,27 @@ export function ConduitHeader({
},
]}
>
-
-
-
+
+
+
+
+
+
+
+ v.{GIT_HASH}
+
+
);
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..8c4ff8b
--- /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 { handleError, wrapError } from "@/src/common/errors";
+import { MBToBytes, bytesToMB } from "@/src/common/utils";
+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";
+
+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/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..5c2909f 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,35 @@
},
"CONNECTED_PEERS_I18N": {
"string": "{{ peers }} connected peers"
+ },
+ "DONE_I18N": {
+ "string": "Done"
+ },
+ "CANCEL_I18N": {
+ "string": "Cancel"
+ },
+ "CONFIRM_I18N": {
+ "string": "Confirm"
+ },
+ "SAVE_I18N": {
+ "string": "Save"
+ },
+ "EDIT_SETTINGS_I18N": {
+ "string": "Edit Settings"
+ },
+ "MAX_PEERS_I18N": {
+ "string": "Max Peers"
+ },
+ "MBPS_PER_PEER_I18N": {
+ "string": "MBps Per Peer"
+ },
+ "MAX_BANDWIDTH_USAGE_I18N": {
+ "string": "Max Bandwidth Usage"
+ },
+ "SEND_DIAGNOSTIC_I18N": {
+ "string": "Send Diagnostic"
+ },
+ "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..a850f90 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 { wrapError } from "@/src/common/errors";
import { Base64Unpadded64Bytes } from "@/src/common/validators";
+import {
+ DEFAULT_INPROXY_LIMIT_BYTES_PER_SECOND,
+ DEFAULT_INPROXY_MAX_CLIENTS,
+} from "@/src/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..ba646d4 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({
@@ -233,6 +233,9 @@ export const sharedStyles = StyleSheet.create({
whiteText: {
color: palette.white,
},
+ greyText: {
+ color: palette.grey,
+ },
blackText: {
color: palette.black,
},
@@ -243,6 +246,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 +276,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 +354,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,
+];