first commit
This commit is contained in:
commit
8234311f75
1
README.md
Normal file
1
README.md
Normal 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
49
app.json
Normal 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
117
app/(auth)/_layout.tsx
Normal 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
13
app/(auth)/login.tsx
Normal 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
11
app/(auth)/signup.tsx
Normal 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
0
app/(chat)/chat.tsx
Normal file
0
app/(profile)/_layout.tsx
Normal file
0
app/(profile)/_layout.tsx
Normal file
73
app/(profile)/addfriend.tsx
Normal file
73
app/(profile)/addfriend.tsx
Normal 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;
|
101
app/(profile)/friendrequests.tsx
Normal file
101
app/(profile)/friendrequests.tsx
Normal 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
113
app/(tabs)/_layout.tsx
Normal 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
108
app/(tabs)/contacts.tsx
Normal 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
116
app/(tabs)/index.tsx
Normal 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 today’s digital landscape, protecting privacy while ensuring ease of use is more important than ever. That’s why we’ve 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, we’ve worked tirelessly to make the application intuitive and user-friendly. We believe privacy shouldn’t 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
168
app/(tabs)/messages.tsx
Normal 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
46
app/+html.tsx
Normal 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
38
app/+not-found.tsx
Normal 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
57
app/CurrentToast.tsx
Normal 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
33
app/Provider.tsx
Normal 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
134
app/_layout.tsx
Normal 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
195
app/chat/[contactid].tsx
Normal 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
40
app/chat/_layout.tsx
Normal 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
31
app/chat/replacement.tsx
Normal 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
22
app/modal.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
BIN
assets/fonts/SpaceMono-Regular.ttf
Normal file
BIN
assets/fonts/SpaceMono-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/images/Gnome_child.png
Normal file
BIN
assets/images/Gnome_child.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
assets/images/adaptive-icon.png
Normal file
BIN
assets/images/adaptive-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/favicon.png
Normal file
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
BIN
assets/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/splash.png
Normal file
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
BIN
assets/wasm/sql-wasm.wasm
Normal file
Binary file not shown.
20
babel.config.js
Normal file
20
babel.config.js
Normal 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
77
biome.json
Normal 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
127
components/crypto.jsx
Normal 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
219
components/dbconnector.jsx
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
21
components/toastwrapper.jsx
Normal file
21
components/toastwrapper.jsx
Normal 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
19
constants/Colors.ts
Normal 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
104
contexts/authcontext.jsx
Normal 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
65
metro.config.js
Normal 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
152
package.json
Normal 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
194
screens/loginscreen.jsx
Normal 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
158
screens/signupscreen.jsx
Normal 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
9
screens/test.js
Normal 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
2962
tamagui-web.css
Normal file
File diff suppressed because one or more lines are too long
4
tamagui.config.ts
Normal file
4
tamagui.config.ts
Normal 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
35
tsconfig.base.json
Normal 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
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user