Skip to content

Commit

Permalink
Merge pull request #8 from nikgraf/ui-styling
Browse files Browse the repository at this point in the history
UI styling
  • Loading branch information
nikgraf committed Jun 9, 2024
2 parents af6bab1 + 5d3b175 commit 10bc7de
Show file tree
Hide file tree
Showing 57 changed files with 2,463 additions and 504 deletions.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,17 @@ TODO better version where the token is also never exposed to the network so not

## Todos

- use expo-secure-store for the sessionKey
- encrypt MMKV storage on iOS and Android
- todo list UI & structure
- nav UI structure
- logo and colors
- deploy to production
- create a new invitation needs a change in react-native-libsodium (use noble?)
- show pending changes in the sidebar and the list

- deploy to production (before move the repo & cleanup readme and make on initial commit)
- logo

- web: focus on input after adding item
- locker bug during registration 🤷
- sort lists in the drawer

- store me and members data also locally
- figure out how author keys are managed (tabs in serenity and possible change in secsync)
- add retry for locker in case write fails (invalid clock)
- allow to delete list (needs a tombstone and properly cleanup local stores)
Expand Down
7 changes: 7 additions & 0 deletions apps/app/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ const config = getDefaultConfig(__dirname, { isCSSEnabled: true });
config.resolver.unstable_enableSymlinks = true;
config.resolver.unstable_enablePackageExports = true;

//.needed for zustand https://github.com/pmndrs/zustand/discussions/1967#discussioncomment-9578159
config.resolver.unstable_conditionNames = [
"browser",
"require",
"react-native",
];

// Needed for monorepo setup (can be removed in standalone projects)
const projectRoot = __dirname;
const monorepoRoot = path.resolve(projectRoot, "../..");
Expand Down
5 changes: 4 additions & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"main": "src/AppEntry.ts",
"version": "1.0.0",
"scripts": {
"dev": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start --dev-client",
"dev": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start --dev-client -c",
"ts:check": "tsc --noEmit",
"test": "jest",
"lint": "echo \"Lint not setup\" # expo lint"
Expand All @@ -13,6 +13,9 @@
},
"dependencies": {
"@expo/vector-icons": "^14.0.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@react-navigation/drawer": "^6.6.15",
"@react-navigation/native": "^6.0.2",
"@tanstack/react-query": "^5.40.0",
"@trpc/client": "^11.0.0-rc.382",
Expand Down
56 changes: 56 additions & 0 deletions apps/app/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { DrawerActions } from "@react-navigation/native";
import { Redirect, useNavigation, usePathname } from "expo-router";
import { Drawer } from "expo-router/drawer";
import { Pressable, useWindowDimensions } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { PanelLeft } from "~/lib/icons/PanelLeft";
import { DrawerContent } from "../../components/drawerContent";
import { useIsPermanentLeftDrawer } from "../../hooks/useIsPermanentDrawer";
import { getSessionKey } from "../../utils/sessionKeyStorage";

export default function Layout() {
const isPermanentLeftDrawer = useIsPermanentLeftDrawer();
const { width: fullWidth } = useWindowDimensions();
const pathname = usePathname();

const sessionKey = getSessionKey();
if (!sessionKey) {
const redirect = pathname !== "/" ? "?redirect=" + pathname : "";
return <Redirect href={`/login${redirect}`} />;
}

return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Drawer
drawerContent={DrawerContent}
screenOptions={{
drawerType: isPermanentLeftDrawer ? "permanent" : "front",
drawerStyle: {
width: isPermanentLeftDrawer ? 240 : fullWidth,
},
overlayColor: "transparent",
drawerPosition: "left",
headerShown: isPermanentLeftDrawer ? false : true,
headerLeft: () => {
const navigation = useNavigation();

return (
<Pressable
className="p-6"
onPress={() =>
navigation.dispatch(DrawerActions.toggleDrawer())
}
>
<PanelLeft className="text-black" />
</Pressable>
);
},
headerTitle: () => null,
// drawerStyle: {
// width: 240,
// },
}}
/>
</GestureHandlerRootView>
);
}
13 changes: 13 additions & 0 deletions apps/app/src/app/(app)/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from "react";
import { View } from "react-native";
import { Text } from "~/components/ui/text";

const Home: React.FC = () => {
return (
<View className="flex flex-1 justify-center items-center">
<Text className="text-4xl">Welcome 🤗</Text>
</View>
);
};

export default Home;
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import * as sodium from "react-native-libsodium";
import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card";
import { Text } from "~/components/ui/text";
import { useLocker } from "../../hooks/useLocker";
import { acceptInvitation } from "../../utils/acceptInvitation";
import { getHashParameter } from "../../utils/getHashParam";
import { getSessionKey } from "../../utils/sessionKeyStorage";
import { trpc } from "../../utils/trpc";
import { useLocker } from "../../../hooks/useLocker";
import { acceptInvitation } from "../../../utils/acceptInvitation";
import { getHashParameter } from "../../../utils/getHashParam";
import { getSessionKey } from "../../../utils/sessionKeyStorage";
import { trpc } from "../../../utils/trpc";

const Invitation: React.FC = () => {
const acceptDocumentInvitationMutation =
trpc.acceptDocumentInvitation.useMutation();
const { token: rawToken } = useLocalSearchParams();
const token = typeof rawToken === "string" ? rawToken : "";
const key = getHashParameter("key");
const locker = useLocker();
const { addItem } = useLocker();

const documentInvitationByTokenQuery =
trpc.documentInvitationByToken.useQuery(token);
Expand All @@ -38,11 +38,6 @@ const Invitation: React.FC = () => {
return;
}

console.log(
"documentInvitationByTokenQuery",
documentInvitationByTokenQuery
);

const { listKey } = acceptInvitation({
ciphertext: documentInvitationByTokenQuery.data.ciphertext,
nonce: documentInvitationByTokenQuery.data.nonce,
Expand All @@ -56,9 +51,9 @@ const Invitation: React.FC = () => {
onError: () => {
alert("Failed to accept invitation. Please try again.");
},
onSuccess: (data) => {
onSuccess: async (data) => {
if (data?.documentId) {
locker.addItem({
await addItem({
type: "document",
documentId: data.documentId,
value: sodium.to_base64(listKey),
Expand Down
38 changes: 38 additions & 0 deletions apps/app/src/app/(app)/list/[listId].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useLocalSearchParams } from "expo-router";
import React from "react";
import { View } from "react-native";
import sodium from "react-native-libsodium";
import { Text } from "~/components/ui/text";
import Document from "../../../components/document";
import { useLocker } from "../../../hooks/useLocker";

const List: React.FC = () => {
const { listId } = useLocalSearchParams();
const documentId = typeof listId === "string" ? listId : null;

const { content } = useLocker();

if (!documentId) {
return (
<View>
<Text>Document not found</Text>
</View>
);
}

const documentKeyBase64 = content[`document:${documentId}`];

if (!documentKeyBase64) {
return (
<View>
<Text>Loading document key …</Text>
</View>
);
}

const documentKey = sodium.from_base64(documentKeyBase64);

return <Document documentId={documentId} documentKey={documentKey} />;
};

export default List;
86 changes: 44 additions & 42 deletions apps/app/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { SplashScreen, Stack } from "expo-router";
import { TRPCClientError, httpBatchLink } from "@trpc/client";
import {
Slot,
SplashScreen,
router,
useNavigationContainerRef,
} from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useEffect, useState } from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { PortalHost } from "~/components/primitives/portal";
import { NAV_THEME } from "~/lib/constants";
import { useColorScheme } from "~/lib/useColorScheme";
import "../global.css";
Expand Down Expand Up @@ -43,42 +49,44 @@ export default function Layout() {
}
}, [isLoadingComplete]);

const navigationRef = useNavigationContainerRef(); // You can also use a regular ref with `React.useRef()`

const [queryClient] = useState(
() =>
new QueryClient({
queryCache: new QueryCache({
// TODO
// onError: (error) => {
// if (
// error instanceof TRPCClientError &&
// error.data?.code === "UNAUTHORIZED" &&
// window.location.pathname !== "/login"
// ) {
// removeLocalDb();
// queryClient.clear();
// router.navigate({
// to: "/login",
// search: { redirect: window.location.pathname },
// });
// }
// },
onError: (error) => {
if (
error instanceof TRPCClientError &&
error.data?.code === "UNAUTHORIZED" &&
!(
navigationRef.getRootState().routes[0].name === "login" ||
navigationRef.getRootState().routes[0].name === "register"
)
) {
queryClient.clear();
// const redirect = pathname !== "/" ? "?redirect=" + pathname : "";
// router.navigate(`/login${redirect}`);
router.navigate(`/`);
}
},
}),
mutationCache: new MutationCache({
// TODO
// onError: (error) => {
// if (
// error instanceof TRPCClientError &&
// error.data?.code === "UNAUTHORIZED" &&
// window.location.pathname !== "/login"
// ) {
// removeLocalDb();
// queryClient.clear();
// router.navigate({
// to: "/login",
// search: { redirect: window.location.pathname },
// });
// }
// },
onError: (error) => {
if (
error instanceof TRPCClientError &&
error.data?.code === "UNAUTHORIZED" &&
!(
navigationRef.getRootState().routes[0].name === "login" ||
navigationRef.getRootState().routes[0].name === "register"
)
) {
queryClient.clear();
// const redirect = pathname !== "/" ? "?redirect=" + pathname : "";
// router.navigate(`/login${redirect}`);
router.navigate(`/`);
}
},
}),
})
);
Expand Down Expand Up @@ -108,17 +116,11 @@ export default function Layout() {
<SafeAreaProvider>
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
<Stack>
<Stack.Screen
name="index"
options={{
// Hide the header for all other routes.
title: "Lists",
}}
/>
</Stack>

<Slot />

{/* Default Portal Host (one per app) */}
{/* <PortalHost /> */}
<PortalHost />
</ThemeProvider>
</SafeAreaProvider>
</QueryClientProvider>
Expand Down
Loading

0 comments on commit 10bc7de

Please sign in to comment.