Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conduit personalization #26

Merged
merged 4 commits into from
Oct 23, 2024
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
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7",
"i18next": "^23.15.1",
"jdenticon": "^3.3.0",
"micro-key-producer": "^0.7.0",
"react": "18.2.0",
"react-i18next": "^15.0.2",
Expand Down
8 changes: 8 additions & 0 deletions src/common/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,12 @@ describe("utils", () => {
expect(utils.byteArraysAreEqual(byteArrayA, byteArrayB)).toBe(false);
expect(utils.byteArraysAreEqual(byteArrayA, byteArrayC)).toBe(false);
});

it("hexToHueDegrees", () => {
expect(utils.hexToHueDegrees("#E0E0E0")).toEqual(0);
expect(utils.hexToHueDegrees("#000000")).toEqual(0);
expect(utils.hexToHueDegrees("#d54028")).toEqual(8);
expect(utils.hexToHueDegrees("#3b7a96")).toEqual(198);
expect(utils.hexToHueDegrees("#5d4264")).toEqual(288);
});
});
54 changes: 48 additions & 6 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ export function uint8ArrayToJsonObject(arr: Uint8Array): any {
return JSON.parse(new TextDecoder().decode(arr));
}

export function niceBytes(bytes: number, errorHandler: (error: Error) => void) {
var units = ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
var unit = units.shift() as string;
export function niceBytes(
bytes: number,
errorHandler: (error: Error) => void,
): string {
let units = ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
let unit = units.shift() as string;
try {
while (units.length > 0 && bytes >= 1024) {
bytes /= 1024;
Expand All @@ -33,12 +36,12 @@ export function niceBytes(bytes: number, errorHandler: (error: Error) => void) {
return `${bytes.toFixed(bytes > 0 ? 1 : 0)} ${unit}`;
}

export function bytesToMB(bytes: number) {
export function bytesToMB(bytes: number): number {
"worklet";
return bytes / 1024 / 1024;
}

export function MBToBytes(MB: number) {
export function MBToBytes(MB: number): number {
"worklet";
return MB * 1024 * 1024;
}
Expand All @@ -51,7 +54,7 @@ export function timedLog(message: string) {
console.log(`${now.toISOString()} (+${diff}): ${message}`);
}

export function drawBigFont(win: ScaledSize) {
export function drawBigFont(win: ScaledSize): boolean {
// used to determine if we should scale font size down for smaller screens
// only currently applied to skia-rendered paragraphs
if (win.height * win.scale > 1000) {
Expand All @@ -60,3 +63,42 @@ export function drawBigFont(win: ScaledSize) {
return false;
}
}

// hexToHueDegrees extracts Hue in degrees from a hex string
// https://en.wikipedia.org/wiki/Hue
export function hexToHueDegrees(hex: string): number {
if (hex.length != 7 || !hex.startsWith("#")) {
console.error("Could not convert hex to hsl, invalid hex format");
return 0;
}

let r = parseInt(hex.substring(1, 3), 16);
let g = parseInt(hex.substring(3, 5), 16);
let b = parseInt(hex.substring(5, 8), 16);
(r /= 255), (g /= 255), (b /= 255);
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);

// "Defining hue in terms of RGB" from wikipedia
let h = 0;
if (max === min) {
// leave as zero
} else {
let d = max - min;
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}

h /= 6;
}

return Math.round(360 * h);
}
108 changes: 108 additions & 0 deletions src/components/ConduitName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as SecureStore from "expo-secure-store";
import React from "react";
import { useTranslation } from "react-i18next";
import { Text, TextInput, View } from "react-native";

import {
QUERYKEY_CONDUIT_NAME,
SECURESTORE_CONDUIT_NAME_KEY,
} from "@/src/constants";
import { palette, sharedStyles as ss } from "@/src/styles";

export function ConduitName() {
const conduitName = useQuery({
queryKey: [QUERYKEY_CONDUIT_NAME],
queryFn: async () => {
console.log("Getting conduit name from secure store");
return await SecureStore.getItemAsync(SECURESTORE_CONDUIT_NAME_KEY);
},
});

if (conduitName.error) {
return <Text>Error</Text>;
}

// empty string "" is falsy, so we check specifically
if (conduitName.data !== null && conduitName.data !== undefined) {
return <EditableConduitName initialName={conduitName.data} />;
} else {
return null;
}
}

// The Conduit Name is purely cosmetic, it is optional and only stored locally
export function EditableConduitName({ initialName }: { initialName: string }) {
const { t } = useTranslation();

const queryClient = useQueryClient();

const mutateConduitName = useMutation({
mutationFn: async (name: string) => {
await SecureStore.setItemAsync(SECURESTORE_CONDUIT_NAME_KEY, name);
console.log("mutate applied");
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERYKEY_CONDUIT_NAME],
});
},
onError: (error) => {
console.error("Failed to mutate conduit name:", error);
},
});

const [value, setValue] = React.useState(initialName);
const [charsUsed, setCharsUsed] = React.useState(initialName.length);
const [showCharsUsed, setShowCharsUsed] = React.useState(false);
const maxLength = 30;

async function onFocus() {
setShowCharsUsed(true);
}

async function onBlur() {
setShowCharsUsed(false);
if (value !== initialName) {
await mutateConduitName.mutateAsync(value);
}
}

function onChangeText(text: string) {
setValue(text);
setCharsUsed(text.length);
}

return (
<View style={[ss.fullHeight, ss.flex, ss.row, ss.alignCenter]}>
<TextInput
style={[
ss.flex,
ss.whiteText,
ss.bodyFont,
ss.midGreyBorder,
ss.rounded10,
ss.paddedHorizontal,
{ height: "100%" },
]}
placeholder={t("NAME_YOUR_CONDUIT_I18N.string")}
placeholderTextColor={palette.midGrey}
onChangeText={onChangeText}
onFocus={onFocus}
onBlur={onBlur}
value={value}
selectionColor={palette.blue}
maxLength={maxLength}
autoCorrect={false}
autoComplete={"off"}
/>
{showCharsUsed && (
<View style={[ss.absoluteBottomRight]}>
<Text style={[ss.greyText, ss.tinyFont]}>
{charsUsed}/{maxLength}
</Text>
</View>
)}
</View>
);
}
64 changes: 45 additions & 19 deletions src/components/ConduitSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { useConduitKeyPair } from "@/src/auth/hooks";
import { wrapError } from "@/src/common/errors";
import { MBToBytes, bytesToMB } from "@/src/common/utils";
import { AnimatedText } from "@/src/components/AnimatedText";
import { ConduitName } from "@/src/components/ConduitName";
import { EditableNumberSlider } from "@/src/components/EditableNumberSlider";
import { Icon } from "@/src/components/Icon";
import { NotificationsStatus } from "@/src/components/NotificationsStatus";
Expand Down Expand Up @@ -262,24 +263,50 @@ export function ConduitSettings() {
...lineItemStyle,
ss.flex,
ss.alignCenter,
ss.justifySpaceBetween,
ss.height120,
ss.column,
]}
>
<Text style={[ss.bodyFont, ss.whiteText]}>
{t("YOUR_ID_I18N.string")}
</Text>
{conduitKeyPair.data ? (
<ProxyID
proxyId={getProxyId(
conduitKeyPair.data,
)}
/>
) : (
<ActivityIndicator
size={"small"}
color={palette.white}
/>
)}
<View
style={[
ss.row,
ss.fullWidth,
ss.justifySpaceBetween,
ss.alignCenter,
]}
>
<Text style={[ss.bodyFont, ss.whiteText]}>
{t("YOUR_CONDUIT_I18N.string")}
</Text>
{conduitKeyPair.data ? (
<View style={[ss.row, ss.alignCenter]}>
<Text
style={[
ss.bodyFont,
ss.whiteText,
]}
>
{t("ID_I18N.string")}:
</Text>
<ProxyID
proxyId={getProxyId(
conduitKeyPair.data,
)}
/>
</View>
) : (
<ActivityIndicator
size={"small"}
color={palette.white}
/>
)}
</View>
<View style={[ss.flex, ss.row, ss.alignCenter]}>
<Text style={[ss.whiteText, ss.bodyFont]}>
{t("ALIAS_I18N.string")}:
</Text>
<ConduitName />
</View>
</View>
<View
style={[
Expand Down Expand Up @@ -496,7 +523,7 @@ export function ConduitSettings() {
transparent={true}
onRequestClose={onSettingsClose}
>
<View style={[ss.modalHalfBottom]}>
<View style={[ss.modalBottom90]}>
<Canvas style={[ss.flex]}>
<RoundedRect
x={0}
Expand All @@ -513,13 +540,12 @@ export function ConduitSettings() {
palette.purple,
palette.black,
palette.black,
palette.black,
]}
/>
</RoundedRect>
</Canvas>
</View>
<View style={[ss.modalHalfBottom]}>
<View style={[ss.modalBottom90]}>
{displayRestartConfirmation ? (
<RestartConfirmation />
) : (
Expand Down
Loading