first commit

This commit is contained in:
ch0ic3 2024-12-15 06:40:35 -08:00
commit 8234311f75
45 changed files with 5639 additions and 0 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
note because this is in a monorepo had to remove react, react-dom, and react-native-web deps and change metro.config.js a bit.

49
app.json Normal file
View File

@ -0,0 +1,49 @@
{
"expo": {
"name": "expo-router-example",
"slug": "expo-router-example",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
"expo-font",
[
"expo-build-properties",
{
"ios": {
"newArchEnabled": true
},
"android": {
"newArchEnabled": true
}
}
]
],
"experiments": {
"typedRoutes": true
}
}
}

117
app/(auth)/_layout.tsx Normal file
View File

@ -0,0 +1,117 @@
import React from 'react';
import { Tabs, Slot, useRouter } from 'expo-router';
import { Anchor, Button, useTheme, XStack, YStack } from 'tamagui';
import { PersonStanding, MessageCircle, LogOut, ArrowLeft, ArrowLeftFromLine } from '@tamagui/lucide-icons';
import { useAuth } from '../../contexts/authcontext'; // Adjust path as needed
export default function TabLayout() {
const theme = useTheme();
const { logout, isLoggedIn } = useAuth();
const redirect = true
const router = useRouter()
const goback = () => {
router.replace("/")
}
const navigateToLogin = () => {
router.replace("/login")
}
const navigateToSignup = () => {
router.replace("/signup")
}
const handleLogout = () => {
logout();
};
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarActiveTintColor: theme.red10.val,
tabBarStyle: {
backgroundColor: theme.background.val,
borderTopColor: theme.borderColor.val,
},
headerStyle: {
backgroundColor: theme.background.val,
borderBottomColor: theme.borderColor.val,
},
headerTintColor: theme.color.val,
}}
>
{/* Home Tab */}
<Tabs.Screen
name="login"
options={{
title: "Login",
headerShown: true,
headerLeft: () => <XStack><Button icon={ArrowLeftFromLine} onPress={goback}></Button></XStack>,
tabBarIcon: ({ color }) => <PersonStanding color={color} />,
headerRight: () => (
!isLoggedIn ? (
<XStack gap="$3">
<Button onPress={navigateToLogin}>Login</Button>
<Button onPress={navigateToSignup}>Sign-Up</Button>
</XStack>
):(<Button onPress={handleLogout}>Logout</Button>)
//<Button onPress={handleLogout} mr="$4" bg="$purple8" color="$purple12">
// Logout
//</Button>
),
}}
/>
<Tabs.Screen
name="signup"
options= {{
title:"signup",
headerShown:true,
headerLeft: () => <XStack><Button icon={ArrowLeftFromLine} onPress={goback}></Button></XStack>,
tabBarIcon: ({ color }) => <PersonStanding color={color} />,
headerRight: () => (
!isLoggedIn ? (
<XStack gap="$3">
<Button onPress={navigateToLogin}>Login</Button>
<Button onPress={navigateToSignup}>Sign-Up</Button>
</XStack>
) : (<Button onPress={handleLogout}>Logout</Button>)
//<Button onPress={handleLogout} mr="$4" bg="$purple8" color="$purple12">
// Logout
//</Button>
),
}}>
</Tabs.Screen>
{/* Conditional Rendering for Messages Tab */}
{isLoggedIn ? (
<Tabs.Screen
name="messages"
options={{
title: 'Messages',
tabBarIcon: ({ color }) => <MessageCircle color={color} />,
}}
/>
) : (
<Tabs.Screen
name="messages"
redirect={true}
options={{
title: 'Messages',
tabBarIcon: ({ color }) => <MessageCircle color={color} />,
}}
/>
)}
</Tabs>
);
}

13
app/(auth)/login.tsx Normal file
View File

@ -0,0 +1,13 @@
import { ExternalLink } from '@tamagui/lucide-icons'
import { Anchor, H2, Input, Paragraph, XStack, YStack } from 'tamagui'
import { ToastControl } from 'app/CurrentToast'
import Test from "screens/test"
import Login from "screens/loginscreen"
import {Slot} from "expo-router"
export default function TabOneScreen() {
return (
<Login/>
)
}

11
app/(auth)/signup.tsx Normal file
View File

@ -0,0 +1,11 @@
import { View, Text } from 'react-native'
import React from 'react'
import Login from "../../screens/signupscreen"
const signup = () => {
return (
<Login></Login>
)
}
export default signup

0
app/(chat)/chat.tsx Normal file
View File

View File

View File

@ -0,0 +1,73 @@
import React, { useState } from 'react';
import { TamaguiProvider, Stack, Input, Button, Text, useToast } from 'tamagui';
const AddFriendScreen = () => {
const [friendUsername, setFriendUsername] = useState('');
const [error, setError] = useState('');
//const toast = useToast();
const handleAddFriend = async () => {
if (!friendUsername.trim()) {
setError('Username cannot be empty.');
return;
}
try {
const yourAuthToken = localStorage.getItem("jwtToken");
const response = await fetch("http://localhost:4000/friend-request", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${yourAuthToken}`, // Include the auth token if required by your `authenticate` middleware
},
body: JSON.stringify({ receiverUsername: friendUsername }),
});
if (!response.ok) {
const errorText = await response.text();
//toast.show(errorText || "Error sending friend request", { type: 'error' });
return;
}
const result = await response.json();
console.log(result)
//toast.show("Friend request sent successfully", { type: 'success' });
setFriendUsername(''); // Clear the input field
} catch (error) {
console.error("Error sending friend request:", error);
//toast.show("Failed to send friend request", { type: 'error' });
}
};
return (
<TamaguiProvider>
<Stack f={1} justifyContent="center" alignItems="center" space="$4" padding="$4">
<Text fontSize="$6" fontWeight="bold">
Add a Friend
</Text>
<Input
placeholder="Enter friend's username"
value={friendUsername}
onChangeText={setFriendUsername}
borderColor={error ? 'red' : '$borderColor'}
borderWidth={2}
borderRadius="$4"
padding="$3"
/>
{error ? (
<Text color="red" fontSize="$2">
{error}
</Text>
) : null}
<Button onPress={handleAddFriend} backgroundColor="$primary" color="white">
Send Friend Request
</Button>
</Stack>
</TamaguiProvider>
);
};
export default AddFriendScreen;

View File

@ -0,0 +1,101 @@
import React, { useEffect, useState } from 'react';
import { TamaguiProvider, Stack, Text, Button, ScrollView } from 'tamagui';
const PendingFriendRequestsScreen = () => {
const [pendingRequests, setPendingRequests] = useState([]);
const [error, setError] = useState('');
const fetchPendingRequests = async () => {
try {
const yourAuthToken = localStorage.getItem("jwtToken");
const response = await fetch("http://localhost:4000/friend-requests/pending", {
method: "GET",
headers: {
"Authorization": `Bearer ${yourAuthToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || "Failed to fetch friend requests");
}
const requests = await response.json();
setPendingRequests(requests);
} catch (err) {
console.error("Error fetching pending friend requests:", err);
setError("Failed to load friend requests.");
}
};
const handleResponse = async (requestId, response) => {
try {
const yourAuthToken = localStorage.getItem("jwtToken");
const res = await fetch("http://localhost:4000/friend-request/respond", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${yourAuthToken}`,
},
body: JSON.stringify({ requestId, response }),
});
if (!res.ok) {
const errorText = await res.text();
throw new Error(errorText || "Failed to process friend request");
}
// Update the UI after a successful response
setPendingRequests((prevRequests) =>
prevRequests.filter((request) => request.id !== requestId)
);
} catch (err) {
console.error(`Error ${response}ing friend request:`, err);
alert(`Failed to ${response} friend request.`);
}
};
useEffect(() => {
fetchPendingRequests();
}, []);
return (
<TamaguiProvider>
<Stack f={1} justifyContent="center" alignItems="center" padding="$4" space="$4">
<Text fontSize="$6" fontWeight="bold">
Pending Friend Requests
</Text>
{error ? (
<Text color="red" fontSize="$2">
{error}
</Text>
) : (
<ScrollView contentContainerStyle={{ padding: 10 }}>
{pendingRequests.map((request) => (
<Stack key={request.id} padding="$3" borderWidth={1} borderRadius="$4" borderColor="$borderColor" space="$2">
<Text fontSize="$4">{request.sender.username}</Text>
<Button
backgroundColor="green"
color="white"
onPress={() => handleResponse(request.id, "accepted")}
>
Accept
</Button>
<Button
backgroundColor="red"
color="white"
onPress={() => handleResponse(request.id, "rejected")}
>
Reject
</Button>
</Stack>
))}
</ScrollView>
)}
</Stack>
</TamaguiProvider>
);
};
export default PendingFriendRequestsScreen;

113
app/(tabs)/_layout.tsx Normal file
View File

@ -0,0 +1,113 @@
import React from 'react';
import { Tabs, useRouter } from 'expo-router';
import { Anchor, Button, useTheme, XStack, YStack, Paragraph } from 'tamagui';
import { PersonStanding, MessageCircle, LogOut } from '@tamagui/lucide-icons';
import { useAuth } from '../../contexts/authcontext'; // Adjust path as needed
export default function TabLayout() {
const theme = useTheme();
const { logout, isLoggedIn } = useAuth();
const router = useRouter();
const navigateToLogin = () => {
router.replace("/login");
};
const addFriendsNav = () => {
router.replace("/addfriend")
}
const seePendingNav = () => {
router.replace("/friendrequests")
}
const navigateToSignup = () => {
router.replace("/signup");
};
const handleLogout = () => {
logout();
};
return (
<Tabs
screenOptions={{
headerShown: true,
tabBarActiveTintColor: theme.red10.val,
tabBarStyle: {
backgroundColor: theme.background.val,
borderTopColor: theme.borderColor.val,
},
headerStyle: {
backgroundColor: theme.background.val,
borderBottomColor: theme.borderColor.val,
},
headerTintColor: theme.color.val,
}}
>
{/* Home Tab */}
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <PersonStanding color={color} />,
headerRight: () => (
!isLoggedIn ? (
<XStack gap="$3">
<Button onPress={navigateToLogin}>Login</Button>
<Button onPress={navigateToSignup}>Sign-Up</Button>
</XStack>
) : (
<Button onPress={handleLogout}>Logout</Button>
)
),
}}
/>
{/* Conditional Rendering for Messages Tab */}
{isLoggedIn ? (
<Tabs.Screen
name="messages"
options={{
title: 'Messages',
tabBarIcon: ({ color }) => <MessageCircle color={color} />,
headerRight: () => (
<XStack>
<Button onPress={addFriendsNav}><Paragraph>Add Friend</Paragraph></Button>
<Button onPress={seePendingNav}><Paragraph>Friend Requests</Paragraph></Button>
</XStack>
)
}}
/>
) : (
<Tabs.Screen
name="messages"
redirect={true}
options={{
title: 'Messages',
tabBarIcon: ({ color }) => <MessageCircle color={color} />,
}}
/>
)}
{/* Conditional Rendering for Contacts Tab */}
{isLoggedIn ? (
<Tabs.Screen
name="contacts" // This should refer to the contacts screen inside the chat folder
options={{
title: 'Contacts',
tabBarIcon: ({ color }) => <PersonStanding color={color} />,
}}
/>
) : (
<Tabs.Screen
name="contacts" // Same path, redirect if not logged in
redirect={true}
options={{
title: 'Contacts',
tabBarIcon: ({ color }) => <MessageCircle color={color} />,
}}
/>
)}
</Tabs>
);
}

108
app/(tabs)/contacts.tsx Normal file
View File

@ -0,0 +1,108 @@
import { useState, useEffect } from 'react';
import { XStack, Paragraph, YStack, Button, Text, H6, Avatar } from 'tamagui';
import { useRouter } from 'expo-router';
export default function ContactsPage() {
const [contacts, setContacts] = useState([]);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const router = useRouter();
// Fetch friends from the server
const fetchFriends = async () => {
try {
const authToken = localStorage.getItem('jwtToken'); // Retrieve JWT token
if (!authToken) {
throw new Error('Authentication required. Please log in.');
}
const response = await fetch('http://localhost:4000/friends', {
method: 'GET',
headers: {
Authorization: `Bearer ${authToken}`,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || 'Failed to fetch contacts');
}
const friends = await response.json();
setContacts(friends);
} catch (err) {
console.error('Error fetching friends:', err);
setError(err.message || 'Failed to load contacts.');
} finally {
setLoading(false);
}
};
// Navigate to chat screen
const navigateToChat = (contact) => {
router.push({
pathname: `/chat/${contact.username}`, // Use dynamic route
params: { friendUsername: contact.username }, // Pass necessary params
});
};
useEffect(() => {
fetchFriends();
}, []);
return (
<YStack flex={1} bg="$background" px="$5" pt="$5">
<H6 fontSize={25} mb="$4" color="$primaryText">
Your Contacts
</H6>
{loading ? (
<Text>Loading contacts...</Text>
) : error ? (
<Text color="red">{error}</Text>
) : contacts.length === 0 ? (
<Text>No contacts found. Add some friends to start chatting!</Text>
) : (
contacts.map((contact) => (
<XStack
key={contact.id}
bg="$backgroundLight"
padding="$4"
borderRadius="$12"
mb="$3"
space="$3"
alignItems="center"
hoverStyle={{ bg: '$gray8' }}
>
{/* Profile Picture */}
<Avatar size={50} circular>
<Avatar.Image
src={`/assets/images/Gnome_child.png`} // Placeholder avatar
alt={`${contact.username}'s avatar`}
/>
</Avatar>
{/* Contact Details */}
<YStack flex={1} alignItems="flex-start">
<Paragraph fontSize={18} fontWeight="bold" color="$primaryText">
{contact.username}
</Paragraph>
<Text fontSize={14} color="$secondaryText">
{contact.status} {/* Placeholder for online/offline */}
</Text>
</YStack>
{/* Message Button */}
<Button
size="$2"
onPress={() => navigateToChat(contact)}
theme="blue"
>
Message
</Button>
</XStack>
))
)}
</YStack>
);
}

116
app/(tabs)/index.tsx Normal file
View File

@ -0,0 +1,116 @@
import { ExternalLink } from '@tamagui/lucide-icons'
import { Anchor, H2, Paragraph, XStack, YStack, Button,Separator ,ScrollView} from 'tamagui'
import {useAuth, } from "contexts/authcontext"
import { useRouter} from "expo-router"
export default function index() {
const router = useRouter()
const redirect = () => {
router.replace("/login")
}
return (
<YStack f={1} ai="center" pb="$1" gap="$1" px="$5" pt="$4" bg="$background">
<H2
ta="center" // Center-align the text
color="$primary" // Use theme color for heading
fontSize="$8" // Larger font size for prominence
>
Welcome to a Privacy-Focused Messaging App
</H2>
<Separator my="$3" color="$borderColor" /> {/* Adds a subtle divider */}
<ScrollView f={1} px="$5" py="$5">
<Paragraph
size="$4" // Default font size for body text
ta="justify" // Justify text for clean alignment
color="$color" // Default color for readability
lineHeight="$4" // Increase line height for better readability
maxWidth={"95%"}
pb="$7"
>
In todays digital landscape, protecting privacy while ensuring ease of use is more important than ever. Thats why weve designed an encrypted, privacy-focused chat application that prioritizes security without compromising on simplicity. Our goal is to provide users with a platform where their conversations remain entirely private, safe from surveillance or unauthorized access.
</Paragraph>
<Paragraph size="$4" ta="justify" color="$color" lineHeight="$4" maxWidth={"95%"} pb="$7">
Unlike many messaging apps that collect user data or depend on centralized servers, our application is built with a commitment to transparency and user control. It is fully open-source, meaning anyone can review the code to ensure there are no hidden vulnerabilities or backdoors. This openness fosters trust within the community while encouraging collaboration to continuously improve the platform.
</Paragraph>
<Paragraph size="$4" ta="justify" color="$color" lineHeight="$4" maxWidth={"95%"} pb="$7">
We also understand the growing demand for self-hosting solutions. By making the application self-hostable, we empower users to take complete control of their data. You can set up the application on your own server, ensuring that your messages, metadata, and other sensitive information remain under your control. This eliminates reliance on third-party services and offers a level of privacy and independence unmatched by traditional messaging platforms.
</Paragraph>
<Paragraph size="$4" ta="justify" color="$color" lineHeight="$4" maxWidth={"95%"} pb="$7">
Despite its advanced privacy features, weve worked tirelessly to make the application intuitive and user-friendly. We believe privacy shouldnt come at the expense of convenience. From a clean interface to seamless functionality, everything has been designed with the user in mind.
</Paragraph>
</ScrollView>
{/* Footer always visible at the bottom */}
<XStack
ai="center"
jc="center"
fw="wrap"
gap="$1.5"
px="$4"
py="$3"
bg="$background" // Match background for consistency
borderTopWidth={1} // Optional: Add a border to distinguish
borderColor="$borderColor" // Match theme
>
<Paragraph fos="$1">By</Paragraph>
<Paragraph fos="$1" px="$1" py="$0.2" col="$blue10" bg="$blue5">
Registering
</Paragraph>
<Paragraph fos="$1">You agree to
<Anchor
href="login"
textDecorationLine="none"
col="$purple10"
fos="$1"
px="$2"
py="$1"
br="$3"
bg="$purple5">
terms and condidtions
</Anchor>
</Paragraph>
<XStack
ai="center"
gap="$2"
px="$1"
py="$1"
br="$3"
>
<Paragraph fos="$1">Learn more by visiting our
<Anchor
href="https://discord.gg/jhHXGJCqaZ"
textDecorationLine="none"
col="$purple10"
fos="$1"
px="$2"
py="$1"
br="$3"
bg="$purple5"
hoverStyle={{ bg: '$purple6' }}
pressStyle={{ bg: '$purple4' }}
>
Discord
</Anchor>
</Paragraph>
</XStack>
</XStack>
</YStack>
);
}

168
app/(tabs)/messages.tsx Normal file
View File

@ -0,0 +1,168 @@
import { useState, useEffect } from 'react';
import { XStack, Paragraph, YStack, Button, H6, Avatar, Text } from 'tamagui';
import { useRouter } from 'expo-router';
export default function MessagesPage() {
const [friends, setFriends] = useState([]);
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const router = useRouter();
const navigateToChat = (contact) => {
router.push({
pathname: `/chat/${contact.username}`, // Use dynamic route
params: { friendUsername: contact.username }, // Pass necessary params
});
};
useEffect(() => {
const fetchFriends = async () => {
try {
const authToken = localStorage.getItem('jwtToken'); // Retrieve JWT token
if (!authToken) {
console.error('User is not authenticated');
return;
}
const response = await fetch('http://localhost:4000/friends', {
headers: {
'Authorization': `Bearer ${authToken}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch friends');
}
const data = await response.json();
setFriends(data); // Set friends list
} catch (err) {
console.error('Error fetching friends:', err);
} finally {
setLoading(false);
}
};
fetchFriends();
}, []);
useEffect(() => {
// Fetch messages for each friend
const fetchMessages = async () => {
const authToken = localStorage.getItem('jwtToken'); // Retrieve JWT token
if (!authToken) {
console.error('User is not authenticated');
return;
}
try {
const newMessages = [];
// Fetch the latest message for each friend
for (let friend of friends) {
console.log(friend)
const response = await fetch(`http://localhost:4000/messages?receiverUsername=${friend.username}`, {
headers: {
'Authorization': `Bearer ${authToken}`,
},
});
if (response.ok) {
const messagesData = await response.json();
if (messagesData.length > 0) {
const latestMessage = messagesData[messagesData.length - 1]; // Get the most recent message
newMessages.push({
username: friend.username,
lastMessage: latestMessage.content,
timestamp: latestMessage.createdAt,
avatarUrl: friend.avatarUrl || 'https://i.pravatar.cc/150?img=1', // Default avatar if not available
});
}
}
}
setMessages(newMessages);
} catch (err) {
console.error('Error fetching messages:', err);
} finally {
setLoading(false);
}
};
if (friends.length > 0) {
fetchMessages();
} else {
setLoading(false); // If no friends, stop loading
}
}, [friends]);
if (loading) {
return <Text backgroundColor={"black"}>Loading...</Text>;
}
return (
<YStack f={1} bg="$background" px="$5" pt="$5">
<H6 fontSize={25} mb="$4" color="$primaryText">People You've Messaged</H6>
{/* Check if there are no friends */}
{friends.length === 0 ? (
<YStack f={1} ai="center" jc="center" mt="$10">
<Text fontSize={18} color="$gray9">
You have no friends yet. Add some friends to start chatting!
</Text>
</YStack>
) : (
// Check if there are no messages
messages.length === 0 ? (
<YStack f={1} ai="center" jc="center" mt="$10">
<Text fontSize={18} color="$gray9">
No messages yet. Start a conversation!
</Text>
</YStack>
) : (
messages.map((message, index) => (
<XStack
key={index}
bg="$backgroundLight"
padding="$4"
borderRadius="$12"
mb="$3"
space="$3"
ai="center"
hoverStyle={{ bg: "$gray8" }} // Hover effect similar to Telegram
>
{/* Profile Picture */}
<Avatar circular size={50}>
<Avatar.Image src={ '/assets/images/Gnome_child.png'} />
</Avatar>
<YStack f={1} ai="flex-start">
{/* Username */}
<Paragraph fontSize={18} fontWeight="bold" color="$primaryText">
{message.username}
</Paragraph>
{/* Last message preview */}
<Text fontSize={14} color="$secondaryText">
{message.lastMessage}
</Text>
</YStack>
{/* Timestamp */}
<YStack ai="flex-end">
<Paragraph fontSize={12} color="$gray10">
{new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</Paragraph>
</YStack>
{/* Button to open chat */}
<Button size="$2" onPress={() => navigateToChat(message)} theme="blue">
Open Chat
</Button>
</XStack>
))
)
)}
</YStack>
);
}

46
app/+html.tsx Normal file
View File

@ -0,0 +1,46 @@
import { ScrollViewStyleReset } from 'expo-router/html'
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<head>
<meta charSet='utf-8' />
<meta httpEquiv='X-UA-Compatible' content='IE=edge' />
{/*
This viewport disables scaling which makes the mobile website act more like a native app.
However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
*/}
<meta
name='viewport'
content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1.00001,viewport-fit=cover'
/>
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
)
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`

38
app/+not-found.tsx Normal file
View File

@ -0,0 +1,38 @@
import { Link, Stack } from 'expo-router'
import { StyleSheet } from 'react-native'
import { View, Text } from 'tamagui'
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View margin={10}>
<Text>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
</View>
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
link: {
marginTop: 15,
paddingVertical: 15,
},
linkText: {
fontSize: 14,
color: '#2e78b7',
},
})

57
app/CurrentToast.tsx Normal file
View File

@ -0,0 +1,57 @@
import { Toast, useToastController, useToastState } from '@tamagui/toast'
import { Button, H4, XStack, YStack, isWeb } from 'tamagui'
export function CurrentToast() {
const currentToast = useToastState()
if (!currentToast || currentToast.isHandledNatively) return null
return (
<Toast
key={currentToast.id}
duration={currentToast.duration}
viewportName={currentToast.viewportName}
enterStyle={{ opacity: 0, scale: 0.5, y: -25 }}
exitStyle={{ opacity: 0, scale: 1, y: -20 }}
y={isWeb ? '$12' : 0}
theme="purple"
br="$6"
animation="quick"
>
<YStack ai="center" p="$2" gap="$2">
<Toast.Title fow="bold">{currentToast.title}</Toast.Title>
{!!currentToast.message && (
<Toast.Description>{currentToast.message}</Toast.Description>
)}
</YStack>
</Toast>
)
}
export function ToastControl() {
const toast = useToastController()
return (
<YStack gap="$2" ai="center">
<H4>Toast demo</H4>
<XStack gap="$2" jc="center">
<Button
onPress={() => {
toast.show('Successfully saved!', {
message: "Don't worry, we've got your data.",
})
}}
>
Show
</Button>
<Button
onPress={() => {
toast.hide()
}}
>
Hide
</Button>
</XStack>
</YStack>
)
}

33
app/Provider.tsx Normal file
View File

@ -0,0 +1,33 @@
import { useColorScheme } from 'react-native'
import { TamaguiProvider, type TamaguiProviderProps } from 'tamagui'
import { ToastProvider, ToastViewport } from '@tamagui/toast'
import { CurrentToast } from './CurrentToast'
import { config } from '../tamagui.config'
import {useRouter} from "expo-router"
export function Provider({ children, ...rest }: Omit<TamaguiProviderProps, 'config'>) {
const colorScheme = useColorScheme()
return (
<TamaguiProvider
config={config}
defaultTheme={colorScheme === 'dark' ? 'dark' : 'light'}
{...rest}
>
<ToastProvider
swipeDirection="horizontal"
duration={6000}
native={
[
/* uncomment the next line to do native toasts on mobile. NOTE: it'll require you making a dev build and won't work with Expo Go */
// 'mobile'
]
}
>
{children}
<CurrentToast />
<ToastViewport top="$8" left={0} right={0} />
</ToastProvider>
</TamaguiProvider>
)
}

134
app/_layout.tsx Normal file
View File

@ -0,0 +1,134 @@
import "../tamagui-web.css"
import { useEffect } from 'react'
import { ToastWrapper } from './../components/toastwrapper'
import { StatusBar, useColorScheme } from 'react-native'
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
import { useFonts } from 'expo-font'
import { SplashScreen, Stack, Slot } from 'expo-router'
import { Provider } from './Provider'
import { useTheme, Button, Anchor, XStack } from 'tamagui'
import { ArrowLeft, ArrowRight } from '@tamagui/lucide-icons';
import {useAuth,AuthProvider} from "contexts/authcontext"
import { useRouter } from 'expo-router'
import { Platform } from 'react-native';
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router'
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(tabs)',
}
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync()
export default function RootLayout() {
const [interLoaded, interError] = useFonts({
Inter: require('@tamagui/font-inter/otf/Inter-Medium.otf'),
InterBold: require('@tamagui/font-inter/otf/Inter-Bold.otf'),
})
useEffect(() => {
const intervalId = setInterval(() => {
console.log("test")
}, 10000)
return () => clearInterval(intervalId)
}, [])
useEffect(() => {
if (interLoaded || interError) {
// Hide the splash screen after the fonts have loaded (or an error was returned) and the UI is ready.
SplashScreen.hideAsync()
}
}, [interLoaded, interError])
if (!interLoaded && !interError) {
return null
}
return (
<>
<AuthProvider>
<Providers>
<ToastWrapper />
<Slot/>
</Providers>
</AuthProvider>
</>
)
}
const Providers = ({ children }: { children: React.ReactNode }) => {
return <Provider>{children}</Provider>
}
function RootLayoutNav() {
const {isLoggedIn, logout,} = useAuth()
const colorScheme = useColorScheme()
const theme = useTheme()
const router = useRouter()
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<StatusBar barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'} />
<Stack>
<Stack.Screen
name="(tabs)"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name='(auth)'
options={{
title: "Authentication",
headerShown:true,
headerLeft: () => (
<XStack>
<Anchor href='/'>
<Button icon={ArrowLeft}></Button>
</Anchor>
</XStack>
),
headerRight: () => (
!isLoggedIn ? (
<XStack gap="$3">
<Button onPress={router.replace("/login")}>Login</Button>
<Anchor href='/signup'>
<Button>Sign-Up</Button>
</Anchor>
</XStack>
):(<Button onPress={logout}>Logout</Button>)
),
}}
>
</Stack.Screen>
<Stack.Screen
name="modal"
options={{
title: 'Tamagui + Expo',
presentation: 'modal',
animation: 'slide_from_right',
gestureEnabled: true,
gestureDirection: 'horizontal',
contentStyle: {
backgroundColor: theme.background.val,
},
}}
/>
</Stack>
</ThemeProvider>
)
}

195
app/chat/[contactid].tsx Normal file
View File

@ -0,0 +1,195 @@
import React, { useEffect, useState } from 'react';
import { useLocalSearchParams } from 'expo-router';
import { YStack, XStack, Text, Input, Button, ScrollView } from 'tamagui';
import { chacha20Encrypt,chacha20Decrypt } from '../../components/crypto';
const ChatApp = () => {
const {friendUsername } = useLocalSearchParams(); // Friend's username passed via params
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState('');
const [loading, setLoading] = useState(true);
const [friendPubKey, setFriendPubKey] = useState(null); // Friend's public key
// Fetch friend's public key
const fetchPublicKey = async () => {
try {
const authToken = localStorage.getItem('jwtToken');
if (!authToken) {
console.error('User is not authenticated');
return;
}
const response = await fetch(`http://localhost:4000/user/pubkey?username=${friendUsername}`, {
headers: {
Authorization: `Bearer ${authToken}`,
},
});
if (!response.ok) throw new Error('Failed to fetch public key');
const { pubKey } = await response.json();
setFriendPubKey(pubKey);
} catch (err) {
console.error('Error fetching public key:', err);
}
};
// Fetch messages
// Fetch messages
const fetchMessages = async () => {
try {
const authToken = localStorage.getItem('jwtToken');
if (!authToken) {
console.error('User is not authenticated');
return;
}
const response = await fetch(`http://localhost:4000/messages/?receiverUsername=${friendUsername}`, {
headers: {
Authorization: `Bearer ${authToken}`,
},
});
if (!response.ok) throw new Error('Failed to fetch messages');
// Fetch encrypted messages as JSON
const encryptedMessages = await response.json();
console.log('Encrypted Messages:', encryptedMessages); // Debug: log encrypted messages
// Decrypt each message and store the decrypted result
const decryptedMessages = await Promise.all(
encryptedMessages.map(async (msg) => {
const decryptedMessage = await chacha20Decrypt(msg);
return { ...msg, content: decryptedMessage }; // Attach decrypted message to original message
})
);
console.log('Decrypted Messages:', decryptedMessages); // Debug: log decrypted messages
// Update the state with the decrypted messages
setMessages(decryptedMessages);
} catch (err) {
console.error('Error fetching messages:', err);
setMessages(["Error fetching messages"]); // Provide a default error message
} finally {
setLoading(false);
}
};
// Fetch initial data
useEffect(() => {
fetchPublicKey();
fetchMessages();
// Poll for new messages every 5 seconds
const intervalId = setInterval(fetchMessages, 1000);
return () => clearInterval(intervalId); // Cleanup on component unmount
}, [friendUsername]);
// Send message
const sendMessage = async () => {
if (!inputText.trim() || !friendPubKey) return;
try {
const authToken = localStorage.getItem('jwtToken');
if (!authToken) {
console.error('User is not authenticated');
return;
}
// Prepare the JSON object for encryption
const messageData = JSON.stringify({
message: inputText,
receiver: friendUsername,
pubkey: friendPubKey,
});
// Encrypt the message using chacha20Encrypt
const encryptedPayload = chacha20Encrypt(messageData);
console.log("payload", JSON.parse(encryptedPayload))
const decrypted = chacha20Decrypt(encryptedPayload)
console.log("decrypted:", decrypted)
// Send the encrypted message to the server
const response = await fetch('http://localhost:4000/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`,
},
body: encryptedPayload, // Send the encrypted payload
});
if (!response.ok) throw new Error('Failed to send message');
const newMessage = await response.json();
setMessages((prevMessages) => [...prevMessages, newMessage]);
setInputText('');
} catch (err) {
console.error('Error sending message:', err);
}
};
return (
<YStack flex={1} bg="$background" padding={10}>
{/* Chat Header */}
<Text fontSize={20} fontWeight="bold" mb={10}>
Chat with {friendUsername}
</Text>
{/* Messages List */}
<ScrollView flex={1} mb={10}>
{loading ? (
<Text>Loading...</Text>
) : (
messages.map((msg) => (
<MessageBubble
key={msg.id}
text={msg.content}
sender={msg.senderUsername === friendUsername ? 'friend' : 'user'}
/>
))
)}
</ScrollView>
{/* Input Field and Send Button */}
<XStack space={10} alignItems="center">
<Input
flex={1}
value={inputText}
onChangeText={setInputText}
placeholder="Type a message..."
onSubmitEditing={sendMessage}
/>
<Button size="large" onPress={sendMessage}>
Send
</Button>
</XStack>
</YStack>
);
};
const MessageBubble = ({ text, sender }) => {
const isUser = sender === 'user';
return (
<XStack
justifyContent={isUser ? 'flex-end' : 'flex-start'}
paddingVertical={5}
paddingHorizontal={10}
>
<YStack
maxWidth="70%"
padding={10}
borderRadius={10}
backgroundColor={isUser ? '#007aff' : '#e5e5ea'}
>
<Text color={isUser ? '#fff' : '#000'}>{text}</Text>
</YStack>
</XStack>
);
};
export default ChatApp;

40
app/chat/_layout.tsx Normal file
View File

@ -0,0 +1,40 @@
import React from 'react';
import { Tabs, useLocalSearchParams, useRouter } from 'expo-router';
import { useAuth } from '../../contexts/authcontext'; // Adjust path if needed
import { Button, useTheme, XStack, YStack, Paragraph } from 'tamagui';
import { PersonStanding, MessageCircle, LogOut, ArrowLeft, ArrowLeftFromLine } from '@tamagui/lucide-icons';
export default function ChatLayout() {
const theme = useTheme();
const { isLoggedIn } = useAuth(); // Ensure the isLoggedIn state is correctly being passed
const router = useRouter();
const contact = useLocalSearchParams(); // Get contactId from URL parameters
const contactId = contact.friendUsername
const goback = () => {
router.replace("/contacts")
}
return (
<Tabs
screenOptions={{
// Hide the tab bar for all screens in this layout
tabBarStyle: {
display: 'none', // This hides the tab bar entirely
},
// Enable dynamic header
headerShown: true,
headerStyle: {
backgroundColor: theme.background.val,
borderBottomColor: theme.borderColor.val,
},
headerTintColor: theme.color.val,
// Set the header title based on the contactId from the URL
title: contactId ? `Chat With ${contactId}` : 'Select a Contact', // Example dynamic title
headerLeft: () => <XStack><Button icon={ArrowLeftFromLine} onPress={goback}></Button></XStack>,
}}
>
</Tabs>
);
}

31
app/chat/replacement.tsx Normal file
View File

@ -0,0 +1,31 @@
import { useLocalSearchParams, useRouter } from 'expo-router';
import { YStack, Paragraph, H1, Avatar } from 'tamagui';
const ChatPage = () => {
const router = useRouter(); // Access route parameters passed from ContactsPage
const contact = useLocalSearchParams()
console.log(contact)
if (!contact) {
return <Paragraph>Something Went Wrong... please go back</Paragraph>;
}
return (
<YStack f={1} bg="$background" px="$5" pt="$5">
<H1 fontSize={30} mb="$4">Chat with {contact.name}</H1>
{/* Display Avatar */}
<Avatar size={60} circular>
<Avatar.Image src={contact.avatarUrl} alt={`${contact.name}'s avatar`} />
</Avatar>
{/* Display Status */}
<Paragraph fontSize={16} color={contact.status === 'online' ? '$green10' : '$gray10'}>
Status: {contact.status}
</Paragraph>
{/* Chat UI would go here */}
</YStack>
);
};
export default ChatPage;

22
app/modal.tsx Normal file
View File

@ -0,0 +1,22 @@
import { Anchor, Paragraph, View, XStack } from 'tamagui'
export default function ModalScreen() {
return (
<View flex={1} alignItems="center" justifyContent="center">
<XStack gap="$2">
<Paragraph ta="center">Made by</Paragraph>
<Anchor col="$blue10" href="https://twitter.com/natebirdman" target="_blank">
@natebirdman,
</Anchor>
<Anchor
color="$purple10"
href="https://github.com/tamagui/tamagui"
target="_blank"
rel="noreferrer"
>
give it a
</Anchor>
</XStack>
</View>
)
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
assets/wasm/sql-wasm.wasm Normal file

Binary file not shown.

20
babel.config.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = (api) => {
api.cache(true)
return {
presets: [['babel-preset-expo', { jsxRuntime: 'automatic' }]],
plugins: [
[
'@tamagui/babel-plugin',
{
components: ['tamagui'],
config: './tamagui.config.ts',
logTimings: true,
disableExtraction: process.env.NODE_ENV === 'development',
},
],
// NOTE: this is only necessary if you are using reanimated for animations
'react-native-reanimated/plugin',
],
}
}

77
biome.json Normal file
View File

@ -0,0 +1,77 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.1/schema.json",
"organizeImports": {
"enabled": false
},
"linter": {
"enabled": true,
"rules": {
"correctness": {
"useExhaustiveDependencies": "off",
"noInnerDeclarations": "off",
"noUnnecessaryContinue": "off",
"noConstructorReturn": "off"
},
"suspicious": {
"noImplicitAnyLet": "off",
"noConfusingVoidType": "off",
"noEmptyInterface": "off",
"noExplicitAny": "off",
"noArrayIndexKey": "off",
"noDoubleEquals": "off",
"noConsoleLog": "error",
"noAssignInExpressions": "off",
"noRedeclare": "off"
},
"style": {
"noParameterAssign": "off",
"noNonNullAssertion": "off",
"noArguments": "off",
"noUnusedTemplateLiteral": "off",
"useDefaultParameterLast": "off",
"useConst": "off",
"useEnumInitializers": "off",
"useTemplate": "off",
"useSelfClosingElements": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off",
"noDangerouslySetInnerHtmlWithChildren": "off"
},
"performance": {
"noDelete": "off",
"noAccumulatingSpread": "off"
},
"complexity": {
"noForEach": "off",
"noBannedTypes": "off",
"useLiteralKeys": "off",
"useSimplifiedLogicExpression": "off",
"useOptionalChain": "off"
},
"a11y": {
"noSvgWithoutTitle": "off",
"useMediaCaption": "off",
"noHeaderScope": "off",
"useAltText": "off",
"useButtonType": "off"
}
}
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 90,
"ignore": ["**/*/generated-new.ts", "**/*/generated-v2.ts"]
},
"javascript": {
"formatter": {
"trailingComma": "es5",
"jsxQuoteStyle": "double",
"semicolons": "asNeeded",
"quoteStyle": "single"
}
}
}

127
components/crypto.jsx Normal file
View File

@ -0,0 +1,127 @@
import { Buffer } from 'buffer';
import { sha256 } from "js-sha256";
import JSChaCha20 from 'js-chacha20';
function bigIntToUint8Array(bigInt, byteLength) {
const hex = bigInt.toString(16).padStart(byteLength * 2, '0');
const byteArray = new Uint8Array(byteLength);
for (let i = 0; i < byteLength; i++) {
byteArray[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return byteArray;
}
export function bigIntToBase64(bigInt) {
const hexString = bigInt.toString(16).padStart(bigInt.toString(16).length + (bigInt.toString(16).length % 2), '0');
return Buffer.from(hexString, 'hex').toString('base64');
}
export function base64ToBigInt(base64) {
return BigInt('0x' + Buffer.from(base64, 'base64').toString('hex'));
}
const pBase64 = "2IGKThw2+H+X0Mc5ZxIfOGX1b3lLc/mDB2Ne73FdoEiJRRfWaETlLUg8dAFUWn4Jxg/QSbP7+f/Q0dZbPCShAXrqWsqxScNk+XvFRTUAhqq1h82Bh4Puok2P1ke6sPR/k+LCt9uHMlt/X0TodDonFIMr87KjB9bQ+zysfPXU2G9chMjqPpY2AkInPHfaMtKtIfBslXKbhGwtaK6t0h0GGJ7W3GU8e1dzo/JgwVDnqoTryAKdoFmrpjg41naQOC5Hl59i+Ik5yEL+NCvSis7IYo55sM6cbu8B4N4wNwDKkefgElADvhSKQJirbmSpPXs5Lr7GXgRBR6t/AGrYTgahPw==";
const gBase64 = "5";
const p = base64ToBigInt(pBase64);
const g = BigInt(gBase64);
export function generatePrivateKey() {
return generateRandomBigInt(p - 1n);
}
function generateRandomBigInt(max) {
let randomHex = '';
while (randomHex.length < max.toString(16).length) {
randomHex += Math.floor(Math.random() * 16).toString(16);
}
return BigInt('0x' + randomHex.slice(0, max.toString(16).length));
}
export function calculatePublicKey(privateKey) {
return modExponentiation(g, privateKey, p);
}
function modExponentiation(base, exponent, modulus) {
let result = BigInt(1);
base = base % modulus;
while (exponent > 0n) {
if (exponent % 2n === 1n) result = (result * base) % modulus;
exponent /= 2n;
base = (base * base) % modulus;
}
return result;
}
export function computeSharedSecret(publicKey, privateKey) {
return modExponentiation(publicKey, privateKey, p);
}
export function chacha20Encrypt(jsonobj) {
try {
let { message, receiver, pubkey } = JSON.parse(jsonobj);
message = new TextEncoder().encode(message);
let storedUsers = JSON.parse(localStorage.getItem("privateKeys")) || [];
const user = storedUsers.find(user => user.username === receiver);
if (!user) return console.error("User not found in localStorage");
const clientPrivKey = base64ToBigInt(user.private64);
const secret = computeSharedSecret(base64ToBigInt(pubkey), clientPrivKey);
const secretArray = bigIntToUint8Array(secret);
const chachaKeyUint8 = new Uint8Array(sha256.create().update(secretArray).digest());
const nonce = generateNonce();
let nonceB64 = Buffer.from(nonce).toString("base64");
const chacha = new JSChaCha20(chachaKeyUint8, nonce);
const encrypted = chacha.encrypt(message);
const encryptedArray = new Uint8Array(encrypted);
const encryptedB64 = Buffer.from(encryptedArray).toString("base64");
return JSON.stringify({
encrypted: encryptedB64,
nonce: nonceB64,
pubKey: pubkey,
receiverUsername: receiver
});
} catch (error) {
console.error("Error during encryption:", error);
return null;
}
}
export function chacha20Decrypt(data) {
try {
const { content, nonce, pubKey, receiverUsername } = data;
let nonceArray = new Uint8Array(Buffer.from(nonce, 'base64'));
if (nonceArray.length !== 12) throw new Error("Nonce should be a 12 byte array!");
let storedUsers = JSON.parse(localStorage.getItem("privateKeys")) || [];
const user = storedUsers.find(user => user.username === receiverUsername);
if (!user) throw new Error("User not found in localStorage for decryption");
const clientPrivKey = base64ToBigInt(user.private64);
const secret = computeSharedSecret(base64ToBigInt(pubKey), clientPrivKey);
const secretArray = bigIntToUint8Array(secret);
const chachaKeyUint8 = new Uint8Array(sha256.create().update(secretArray).digest());
let encryptedArray = new Uint8Array(Buffer.from(content, 'base64'));
const chacha = new JSChaCha20(chachaKeyUint8, nonceArray);
const decrypted = chacha.decrypt(encryptedArray);
return new TextDecoder().decode(decrypted);
} catch (error) {
console.error("Decryption Error:", error);
return "Error decrypting message: " + error.message;
}
}
export function generateNonce(length = 12) {
const nonce = new Uint8Array(length);
for (let i = 0; i < length; i++) nonce[i] = Math.floor(Math.random() * 256);
return nonce;
}

219
components/dbconnector.jsx Normal file
View File

@ -0,0 +1,219 @@
import { Platform } from 'react-native';
import initSqlJs from 'sql.js'; // Load sql.js for Web
// For React Native, use react-native-sqlite-storage
let SQLite;
if (Platform.OS !== 'web') {
SQLite = require('react-native-sqlite-storage'); // Load for React Native
SQLite.enablePromise(true); // Enable promise support for React Native SQLite
}
let db;
// Initialize the SQLite database
export const initializeDatabase = async () => {
try {
if (Platform.OS !== 'web') {
// React Native version using react-native-sqlite-storage
db = await SQLite.openDatabase({ name: 'app.db', location: 'default' });
// Create tables for React Native
await db.executeSql(`
CREATE TABLE IF NOT EXISTS Clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL
)
`);
await db.executeSql(`
CREATE TABLE IF NOT EXISTS FriendList (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
public_key TEXT NOT NULL
)
`);
await db.executeSql(`
CREATE TABLE IF NOT EXISTS Messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_username TEXT NOT NULL,
receiver_username TEXT NOT NULL,
message TEXT NOT NULL,
nonce TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
} else {
// Web version using sql.js
const SQL = await initSqlJs({
locateFile: (file) => {
// Correct the path to sql-wasm.wasm based on where it's served in the public folder
if (file === "sql-wasm.wasm") {
return "/assets/wasm/sql-wasm.wasm"; // This assumes it's in the public folder and served correctly
}
return file;
}
});
// Create an in-memory database for the web
db = new SQL.Database();
// Create tables for Web
db.run(`
CREATE TABLE IF NOT EXISTS Clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
public_key TEXT NOT NULL,
private_key TEXT NOT NULL
)
`);
db.run(`
CREATE TABLE IF NOT EXISTS FriendList (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
public_key TEXT NOT NULL
)
`);
db.run(`
CREATE TABLE IF NOT EXISTS Messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sender_username TEXT NOT NULL,
receiver_username TEXT NOT NULL,
message TEXT NOT NULL,
nonce TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
}
console.log('Database initialized and tables created');
} catch (err) {
console.error('Error initializing database:', err);
throw err;
}
};
// CRUD Operations for React Native and Web
export const addClient = async (username, publicKey, privateKey) => {
try {
if (Platform.OS !== 'web') {
// React Native version using react-native-sqlite-storage
await db.executeSql(
`INSERT INTO Clients (username, public_key, private_key) VALUES (?, ?, ?)`,
[username, publicKey, privateKey]
);
} else {
// Web version using sql.js
db.run(
`INSERT INTO Clients (username, public_key, private_key) VALUES (?, ?, ?)`,
[username, publicKey, privateKey]
);
}
console.log('Client added successfully');
} catch (err) {
console.error('Error adding client:', err);
}
};
export const getAllFriends = async () => {
try {
let results;
if (Platform.OS !== 'web') {
// React Native version using react-native-sqlite-storage
results = await db.executeSql(`SELECT * FROM FriendList`);
return results[0].rows.raw(); // Return friends for React Native
} else {
// Web version using sql.js
results = db.exec(`SELECT * FROM FriendList`);
return results[0].values; // Return friends for Web
}
} catch (err) {
console.error('Error retrieving friends:', err);
}
};
export const getMessages = async (username) => {
try {
let results;
if (Platform.OS !== 'web') {
// React Native version using react-native-sqlite-storage
results = await db.executeSql(
`SELECT * FROM Messages WHERE sender_username = ? OR receiver_username = ? ORDER BY timestamp ASC`,
[username, username]
);
return results[0].rows.raw(); // Return messages for React Native
} else {
// Web version using sql.js
results = db.exec(
`SELECT * FROM Messages WHERE sender_username = ? OR receiver_username = ? ORDER BY timestamp ASC`,
[username, username]
);
return results[0].values; // Return messages for Web
}
} catch (err) {
console.error('Error retrieving messages:', err);
}
};
// Close the database
export const closeDatabase = () => {
try {
if (Platform.OS !== 'web') {
// React Native version using react-native-sqlite-storage
db.close();
} else {
// Web version using sql.js (no explicit close method, just stop using the db)
console.log('Database is in-memory on the web, no close method.');
}
console.log('Database closed');
} catch (err) {
console.error('Error closing database:', err);
}
};
export const getClient = async (username) => {
try {
let results;
if (Platform.OS !== 'web') {
// React Native version using react-native-sqlite-storage
results = await db.executeSql(
`SELECT * FROM Clients WHERE username = ?`,
[username]
);
if (results[0].rows.length > 0) {
const client = results[0].rows.item(0); // Get the first matching row
return {
username: client.username,
public_key: client.public_key,
private_key: client.private_key,
};
} else {
console.warn('Client not found for username:', username);
return null;
}
} else {
// Web version using sql.js
results = db.exec(`SELECT * FROM Clients WHERE username = ?`, [username]);
if (results.length > 0 && results[0].values.length > 0) {
const client = results[0].values[0]; // Get the first matching row
return {
username: client[1], // The second column is the username
public_key: client[2], // The third column is the public_key
private_key: client[3], // The fourth column is the private_key
};
} else {
console.warn('Client not found for username:', username);
return null;
}
}
} catch (err) {
console.error('Error retrieving client:', err);
}
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { Platform } from 'react-native';
import Toast from 'react-native-toast-message';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
export const showToast = (type, title, message) => {
if (Platform.OS === 'web') {
toast[type](<><strong>{title}</strong><br />{message}</>);
} else {
Toast.show({
type,
text1: title,
text2: message,
});
}
};
export const ToastWrapper = () => {
return Platform.OS === 'web' ? <ToastContainer /> : <Toast />;
};

19
constants/Colors.ts Normal file
View File

@ -0,0 +1,19 @@
const tintColorLight = '#2f95dc';
const tintColorDark = '#fff';
export default {
light: {
text: '#000',
background: '#fff',
tint: tintColorLight,
tabIconDefault: '#ccc',
tabIconSelected: tintColorLight,
},
dark: {
text: '#fff',
background: '#000',
tint: tintColorDark,
tabIconDefault: '#ccc',
tabIconSelected: tintColorDark,
},
};

104
contexts/authcontext.jsx Normal file
View File

@ -0,0 +1,104 @@
import { useRouter } from 'expo-router';
import React, { createContext, useContext, useState } from 'react';
import { Alert, Platform } from 'react-native';
import * as SecureStore from "expo-secure-store"
import * as crypto from "expo-crypto"
import {showToast} from "./../components/toastwrapper"
// Create Context for Auth State
export const AuthContext = createContext();
export const register = async (email,username,password, pubKey) => {
const apiurl = "http://localhost:4000/register"
const requestData = {
email,username,password,pubKey
}
try{
const response = await fetch(apiurl, {
method:"POST",
headers: {
"Content-Type": "application/json", // Specify JSON format
},
body: JSON.stringify(requestData)
})
if (!response.ok) {
const errorData = await response.text()
if (errorData === "User already exists") {
showToast('error', 'Sign-up Failed', 'Username taken')
}
console.log(errorData)
return false
} else if (response.ok){
return true
}
}catch (err){
console.log(err)
}
}
export const realLogin = async (username,password) => {
const apiurl = "http://localhost:4000/login"
const requestData = {
username,password
}
try{
const response = await fetch(apiurl, {
method:"POST",
headers: {
"Content-Type": "application/json", // Specify JSON format
},
body: JSON.stringify(requestData)
})
if (!response.ok) {
const errorData = await response.text()
console.log(errorData)
return false
} else if (response.ok){
console.log("Logged In")
const resData = await response.json()
console.log("ServerKey",resData.serverPubKey)
if (Platform.OS ==="web") {
localStorage.setItem("username", username)
localStorage.setItem("jwtToken", resData.token)
localStorage.setItem("serverPubKey", resData.serverPubKey)
} else {
await SecureStore.setItemAsync("jwtToken",resData.token)
}
return true
}
}catch (err){
console.log(err)
}
}
// Custom Hook to use AuthContext
export const useAuth = () => useContext(AuthContext);
// AuthProvider to provide AuthContext to the app
export const AuthProvider = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false); // Default: Not logged in
const [jsontoken, setjsontoken] = useState("")
const login = () => setIsLoggedIn(true); // Set user as logged in
const logout = () => setIsLoggedIn(false); // Set user as logged out
return (
<AuthContext.Provider value={{ isLoggedIn,jsontoken, login, logout }}>
{children}
</AuthContext.Provider>
);
};

65
metro.config.js Normal file
View File

@ -0,0 +1,65 @@
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const { withTamagui } = require('@tamagui/metro-plugin')
const config = getDefaultConfig(__dirname)
// metro.config.js
const path = require('path');
module.exports = {
resolver: {
extraNodeModules: {
crypto: require.resolve('crypto-browserify'),
process: require.resolve('process/browser'),
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer'),
util: require.resolve('util'),
},
},
};
// Add Tamagui to the Metro config
module.exports = withTamagui(config, {
components: ['tamagui'], // Optional: Specify the components to optimize
config: './tamagui.config.ts', // Path to your Tamagui config
outputCSS: './tamagui-web.css', // Optional: Output CSS for web support
})
// REMOVE THIS (just for tamagui internal devs to work in monorepo):
// if (process.env.IS_TAMAGUI_DEV && __dirname.includes('tamagui')) {
// const fs = require('fs')
// const path = require('path')
// const projectRoot = __dirname
// const monorepoRoot = path.resolve(projectRoot, '../..')
// config.watchFolders = [monorepoRoot]
// config.resolver.nodeModulesPaths = [
// path.resolve(projectRoot, 'node_modules'),
// path.resolve(monorepoRoot, 'node_modules'),
// ]
// // have to manually de-deupe
// try {
// fs.rmSync(path.join(projectRoot, 'node_modules', '@tamagui'), {
// recursive: true,
// force: true,
// })
// } catch {}
// try {
// fs.rmSync(path.join(projectRoot, 'node_modules', 'tamagui'), {
// recursive: true,
// force: true,
// })
// } catch {}
// try {
// fs.rmSync(path.join(projectRoot, 'node_modules', 'react'), {
// recursive: true,
// force: true,
// })
// } catch {}
// try {
// fs.rmSync(path.join(projectRoot, 'node_modules', 'react-dom'), {
// recursive: true,
// force: true,
// })
// } catch {}
// }

152
package.json Normal file
View File

@ -0,0 +1,152 @@
{
"name": "123",
"main": "expo-router/entry",
"version": "1.0.0",
"packageManager": "yarn@4.5.0",
"private": true,
"scripts": {
"upgrade:tamagui": "yarn up '*tamagui*'@latest '@tamagui/*'@latest",
"upgrade:tamagui:canary": "yarn up '*tamagui*'@canary '@tamagui/*'@canary",
"check:tamagui": "tamagui check",
"start": "expo start -c",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"test": "jest --watchAll"
},
"jest": {
"preset": "jest-expo"
},
"dependencies": {
"123": "file:",
"@react-navigation/native": "^6.1.17",
"@tamagui/config": "^1.117.0",
"@tamagui/lucide-icons": "^1.117.0",
"@tamagui/toast": "^1.117.0",
"assert": "^1.1.1",
"babel-preset-expo": "^11.0.6",
"browserify-zlib": "^0.1.4",
"buffer": "^4.9.2",
"burnt": "^0.12.2",
"console-browserify": "^1.1.0",
"constants-browserify": "^1.0.0",
"crypto-browserify": "^3.12.1",
"diffie-hellman": "^5.0.3",
"dns.js": "^1.0.1",
"domain-browser": "^1.1.1",
"events": "^1.0.0",
"expo": "~51.0.32",
"expo-build-properties": "~0.12.5",
"expo-crypto": "^14.0.1",
"expo-font": "~12.0.10",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.23",
"expo-secure-store": "^14.0.0",
"expo-splash-screen": "~0.27.5",
"expo-status-bar": "^1.12.1",
"expo-system-ui": "~3.0.7",
"expo-web-browser": "~13.0.3",
"https-browserify": "^0.0.1",
"js-chacha20": "^1.1.0",
"js-sha256": "^0.11.0",
"jsonwebtoken": "^9.0.2",
"node-libs-react-native": "^1.2.1",
"path-browserify": "^0.0.0",
"prisma": "^5.22.0",
"process": "^0.11.10",
"querystring-es3": "^0.2.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.75.2",
"react-native-crypto": "^2.2.0",
"react-native-get-random-values": "^1.11.0",
"react-native-level-fs": "^3.0.0",
"react-native-os": "^1.0.1",
"react-native-randombytes": "^3.6.1",
"react-native-reanimated": "~3.15.1",
"react-native-safe-area-context": "4.10.9",
"react-native-screens": "~3.34.0",
"react-native-sqlite-storage": "^6.0.1",
"react-native-svg": "15.6.0",
"react-native-tcp": "^3.2.1",
"react-native-toast-message": "^2.2.1",
"react-native-udp": "^2.1.0",
"react-native-web": "^0.19.12",
"react-toastify": "^10.0.6",
"readable-stream": "^1.0.33",
"sql-js": "^0.1.0",
"sql.js": "^1.12.0",
"stream-browserify": "^3.0.0",
"string_decoder": "^0.10.31",
"tamagui": "^1.117.2",
"timers-browserify": "^1.0.1",
"tty-browserify": "^0.0.0",
"url": "^0.10.3",
"util": "^0.10.4",
"vm-browserify": "^0.0.4"
},
"devDependencies": {
"@babel/core": "^7.24.6",
"@expo/metro-config": "~0.18.4",
"@expo/metro-runtime": "~3.2.1",
"@tamagui/babel-plugin": "^1.117.0",
"@tamagui/cli": "^1.117.0",
"@tamagui/metro-plugin": "^1.117.2",
"@types/react": "~18.2.79",
"@types/react-native": "^0.73.0",
"@types/react-native-get-random-values": "^1",
"typescript": "^5.5.2"
},
"react-native": {
"zlib": "browserify-zlib",
"console": "console-browserify",
"constants": "constants-browserify",
"crypto": "react-native-crypto",
"dns": "dns.js",
"net": "react-native-tcp",
"domain": "domain-browser",
"http": "react-native-http",
"https": "https-browserify",
"os": "react-native-os",
"path": "path-browserify",
"querystring": "querystring-es3",
"fs": "react-native-level-fs",
"_stream_transform": "readable-stream/transform",
"_stream_readable": "readable-stream/readable",
"_stream_writable": "readable-stream/writable",
"_stream_duplex": "readable-stream/duplex",
"_stream_passthrough": "readable-stream/passthrough",
"dgram": "react-native-udp",
"stream": "stream-browserify",
"timers": "timers-browserify",
"tty": "tty-browserify",
"vm": "vm-browserify",
"tls": false
},
"browser": {
"_stream_duplex": "readable-stream/duplex",
"_stream_passthrough": "readable-stream/passthrough",
"_stream_readable": "readable-stream/readable",
"_stream_transform": "readable-stream/transform",
"_stream_writable": "readable-stream/writable",
"console": "console-browserify",
"constants": "constants-browserify",
"crypto": "react-native-crypto",
"dgram": "react-native-udp",
"dns": "dns.js",
"domain": "domain-browser",
"fs": "react-native-level-fs",
"http": "react-native-http",
"https": "https-browserify",
"net": "react-native-tcp",
"os": "react-native-os",
"path": "path-browserify",
"querystring": "querystring-es3",
"stream": "stream-browserify",
"timers": "timers-browserify",
"tls": false,
"tty": "tty-browserify",
"vm": "vm-browserify",
"zlib": "browserify-zlib"
}
}

194
screens/loginscreen.jsx Normal file
View File

@ -0,0 +1,194 @@
import { ExternalLink, User } from '@tamagui/lucide-icons';
import React, { useState } from 'react';
import { Button, Anchor, H1, H6, Input, Paragraph, XStack, YStack } from 'tamagui';
import Toast from 'react-native-toast-message';
import { useAuth, realLogin } from './../contexts/authcontext';
import { useRouter } from 'expo-router';
import { showToast } from './../components/toastwrapper';
import {
base64ToBigInt,
bigIntToBase64,
generatePrivateKey,
calculatePublicKey,
} from './../components/crypto';
export default function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [email, setEmail] = useState('');
const [err, setErr] = useState('');
const [showPassword, setShowPassword] = useState(false);
const router = useRouter();
const { login, isLoggedIn } = useAuth(false);
const handleKeyPress = (e) => {
if (e.nativeEvent.key === 'Enter') {
handleLogin();
}
};
// Sync the public key to the backend
const syncPublicKeyWithBackend = async (username, publicKey, token) => {
try {
const response = await fetch(`http://localhost:4000/api/updatePublicKey`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ pubKey: publicKey }),
});
if (!response.ok) {
throw new Error('Failed to sync public key with the backend');
}
console.log('Public key synced successfully with the backend');
} catch (error) {
console.error('Error syncing public key:', error);
throw error;
}
};
// Handle the login logic
const handleLogin = async () => {
setErr('');
console.log(email, username, password);
try {
const loginSuccess = await realLogin(username, password); // Authenticate user
console.log('Login Success', loginSuccess);
if (loginSuccess) {
try {
let storedUsers = JSON.parse(localStorage.getItem('privateKeys')) || [];
let existingUser = storedUsers.find(user => user.username === username);
// If no local key exists, generate a new one
if (!existingUser) {
console.log('No local keys found. Generating new key pair...');
const privateKey = generatePrivateKey(); // Generate new private key
const publicKey = calculatePublicKey(privateKey); // Calculate the public key
const private64 = bigIntToBase64(privateKey);
const public64 = bigIntToBase64(publicKey);
// Save locally
const newUser = { username, private64, public64 };
storedUsers.push(newUser);
localStorage.setItem('privateKeys', JSON.stringify(storedUsers));
// Sync the public key with backend
await syncPublicKeyWithBackend(username, publicKey, localStorage.getItem("jwtToken")); // Pass the token you get from the login response
} else {
console.log('Local key found for user:', existingUser);
}
// Perform post-login actions (like initializing the database)
login(); // Update the app context to reflect logged-in state
router.replace('/');
} catch (err) {
console.error('Error during key management or database initialization:', err);
}
} else {
showToast('error', 'Login Failed', 'Invalid email, username, or password');
}
} catch (err) {
console.error('Error during login:', err);
setErr('An error occurred while logging in');
}
};
return (
<YStack f={1} ai="center" gap="$2" px="$10" pt="$5" bg="$background">
{!isLoggedIn ? (
<>
<Button onPress={login}>Login Test</Button>
<H1 pt="5%" fontSize={45}>Login</H1>
<XStack px="$1" py="$2">
<H6 fontSize={15}>Username</H6>
</XStack>
<XStack ai="center" px="$1" pb="$4" style={{ color: 'white' }} bg="">
<Input
bg="white"
color={'black'}
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
</XStack>
<XStack pt="$1">
<H6 fontSize={15}>Password</H6>
</XStack>
<XStack>
<Input
ml="$7"
px="$4"
bg="white"
color={'black'}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
onKeyPress={handleKeyPress}
/>
<Button
ml="$2"
marginTop={12}
size="$1"
bg="white"
color={'black'}
onPress={() => setShowPassword(prev => !prev)}
>
{showPassword ? 'Hide' : 'Show'}
</Button>
</XStack>
<XStack>
<Button onPress={handleLogin}>Login</Button>
</XStack>
<XStack>
<Anchor hoverStyle={{ color: 'red' }} href="/forgot">Forgot password?</Anchor>
</XStack>
<XStack ai="center" jc="center" fw="wrap" gap="$1.5" pos="absolute" b="$2">
<Paragraph fos="$5">Add</Paragraph>
<Paragraph fos="$1" px="$2" py="$1" col="$blue10" bg="$blue5">
tamagui.config.ts
</Paragraph>
<Paragraph fos="$5">to root and follow the</Paragraph>
<XStack
ai="center"
gap="$1.5"
px="$2"
py="$1"
br="$3"
bg="$purple5"
hoverStyle={{ bg: '$purple6' }}
pressStyle={{ bg: '$purple4' }}
>
<Anchor
href="https://tamagui.dev/docs/core/configuration"
textDecorationLine="none"
col="$purple10"
fos="$5"
>
Configuration guide
</Anchor>
<ExternalLink size="$1" col="$purple10" />
</XStack>
<Paragraph fos="$5" ta="center">
to configure your themes and tokens.
</Paragraph>
</XStack>
</>
) : (
<>
<H1>You're already logged in</H1>
</>
)}
</YStack>
);
}

158
screens/signupscreen.jsx Normal file
View File

@ -0,0 +1,158 @@
import { ExternalLink, User } from '@tamagui/lucide-icons'
import React, {useState} from 'react'
import {Button, Anchor,H1, H6, Input, Paragraph, XStack, YStack } from 'tamagui'
import { ToastControl } from 'app/CurrentToast'
import {useAuth, register} from "./../contexts/authcontext"
import { useRouter } from 'expo-router'
import {showToast} from "./../components/toastwrapper"
import { base64ToBigInt, bigIntToBase64, generatePrivateKey, calculatePublicKey, computeSharedSecret, generateNonce, chacha20Encrypt, chacha20Decrypt } from "./../components/crypto";
import {initializeDatabase, addClient,getClient, closeDatabase} from "./../components/dbconnector"
export default function Register() {
const [username, setUsername] = useState("")
const [password, setpassword] = useState("")
const [email, setemail] = useState("")
const [err, seterr] = useState("")
const [showpassword, setshowpassword] = useState(false)
const router = useRouter()
const {isloggedin } = useAuth()
const handleLogin = async () => {
seterr('');
console.log(email, username, password);
const clientPrivKey = generatePrivateKey();
const clientPubKey = calculatePublicKey(clientPrivKey);
try {
const loginSuccess = await register(email, username, password, bigIntToBase64(clientPubKey));
if (loginSuccess)
{
let storedPrivateKeys =JSON.parse(localStorage.getItem("Keys")) || []
const existingUser = storedPrivateKeys.find(user => user.username === username);
if (existingUser) {
console.error("User already exists");
return;
}
const private64 = bigIntToBase64(clientPrivKey)
const public64 = bigIntToBase64(clientPubKey)
storedPrivateKeys.push({username,private64,public64})
localStorage.setItem("privateKeys", JSON.stringify(storedPrivateKeys))
// Initialize the database and add the client
//await initializeDatabase();
//await addClient(username, clientPubKey, clientPrivKey);
//closeDatabase();
showToast("success", "successfully registered", "go to the login page and login")
setTimeout(() => {
router.replace("/login")
}, 3000)
}else {
setUsername("")
setpassword("")
setemail("")
}
} catch (err) {
console.error(err);
seterr('An error occurred while logging in');
}
};
return (
<YStack f={1} ai="center" gap="$2" px="$10" pt="$5" bg="$background">
<H1 pt="5%" fontSize={45}>Register</H1>
<XStack px="$1" py="$2">
<H6 fontSize={15}>
Username
</H6>
</XStack>
<XStack ai="center" px="$1" pb="$8" style={{color:"white"}} bg="" >
<Input bg="white" color={'black'}
placeholder='Username'
value={username}
onChangeText={setUsername}
></Input>
</XStack>
<XStack px="$1" py="$2">
<H6 px="$1">
Email
</H6>
</XStack>
<XStack ai="center" px="$1" pb="$1" style={{color:"white"}} bg="" >
<Input bg="white" color={'black'}
placeholder='Email'
value={email}
onChangeText={setemail}
></Input>
</XStack>
<XStack>
<Paragraph>Password</Paragraph>
</XStack>
<XStack>
<Input px="$4" bg="white" color={"black"}
placeholder='Password'
value ={password}
onChangeText={setpassword}
secureTextEntry = {!showpassword}
></Input>
<Button
marginTop={12}
size="$1"
bg="white"
color={'black'}
onPress={() => setshowpassword((prev) => !prev)}
>{showpassword ? "hide":"show"}</Button>
</XStack>
<XStack>
<Button onPress={handleLogin}>Login</Button>
</XStack>
<XStack>
<Anchor hoverStyle={{color:"red"}} href='/forgot'>Forgot password?</Anchor>
</XStack>
<XStack ai="center" jc="center" fw="wrap" gap="$1.5" pos="absolute" b="$2">
<Paragraph fos="$5">Add</Paragraph>
<Paragraph fos="$1" px="$2" py="$1" col="$blue10" bg="$blue5">
tamagui.config.ts
</Paragraph>
<Paragraph fos="$5">to root and follow the</Paragraph>
<XStack
ai="center"
gap="$1.5"
px="$2"
py="$1"
br="$3"
bg="$purple5"
hoverStyle={{ bg: '$purple6' }}
pressStyle={{ bg: '$purple4' }}
>
<Anchor
href="https://tamagui.dev/docs/core/configuration"
textDecorationLine="none"
col="$purple10"
fos="$5"
>
Configuration guide
</Anchor>
<ExternalLink size="$1" col="$purple10" />
</XStack>
<Paragraph fos="$5" ta="center">
to configure your themes and tokens.
</Paragraph>
</XStack>
</YStack>
)
}

9
screens/test.js Normal file
View File

@ -0,0 +1,9 @@
import { Anchor, H2, Input, Paragraph, XStack, YStack } from 'tamagui'
const Test = () => {
return(
<H2>testing this 4eva</H2>
)
}
export default Test;

2962
tamagui-web.css Normal file

File diff suppressed because one or more lines are too long

4
tamagui.config.ts Normal file
View File

@ -0,0 +1,4 @@
import { createTamagui } from '@tamagui/core'
import { config as defaultConfig } from '@tamagui/config'
export const config = createTamagui(defaultConfig)

35
tsconfig.base.json Normal file
View File

@ -0,0 +1,35 @@
{
"compilerOptions": {
"baseUrl": ".",
"rootDir": ".",
"importHelpers": true,
"allowJs": false,
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"esModuleInterop": true,
"preserveSymlinks": true,
"incremental": true,
"jsx": "react-jsx",
"module": "system",
"moduleResolution": "node",
"noEmitOnError": false,
"noImplicitAny": false,
"noImplicitReturns": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"useUnknownInCatchVariables": false,
"preserveConstEnums": true,
// DONT DO THIS so jsdoc will remain
"removeComments": false,
"skipLibCheck": true,
"sourceMap": false,
"strictNullChecks": true,
"target": "es2020",
"types": ["node"],
"lib": ["dom", "esnext"]
},
"exclude": ["_"],
"typeAcquisition": {
"enable": true
}
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.base",
"compilerOptions": {
"strict": true
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}