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, +];