commit 8234311f75b7626a265c54b13df655f885613bd3 Author: ch0ic3 Date: Sun Dec 15 06:40:35 2024 -0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2e026c --- /dev/null +++ b/README.md @@ -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. diff --git a/app.json b/app.json new file mode 100644 index 0000000..933f40e --- /dev/null +++ b/app.json @@ -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 + } + } +} diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx new file mode 100644 index 0000000..66c02e8 --- /dev/null +++ b/app/(auth)/_layout.tsx @@ -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 ( + + {/* Home Tab */} + , + tabBarIcon: ({ color }) => , + headerRight: () => ( + !isLoggedIn ? ( + + + + + + + + + + + ):() + + // + ), + }} + /> + , + tabBarIcon: ({ color }) => , + headerRight: () => ( + !isLoggedIn ? ( + + + + + ) : () + + // + ), + + }}> + + + + {/* Conditional Rendering for Messages Tab */} + {isLoggedIn ? ( + , + }} + /> + ) : ( + , + }} + /> + )} + + ); +} diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx new file mode 100644 index 0000000..f671877 --- /dev/null +++ b/app/(auth)/login.tsx @@ -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 ( + + ) +} diff --git a/app/(auth)/signup.tsx b/app/(auth)/signup.tsx new file mode 100644 index 0000000..38c4bb7 --- /dev/null +++ b/app/(auth)/signup.tsx @@ -0,0 +1,11 @@ +import { View, Text } from 'react-native' +import React from 'react' +import Login from "../../screens/signupscreen" + +const signup = () => { + return ( + + ) +} + +export default signup \ No newline at end of file diff --git a/app/(chat)/chat.tsx b/app/(chat)/chat.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/(profile)/_layout.tsx b/app/(profile)/_layout.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/(profile)/addfriend.tsx b/app/(profile)/addfriend.tsx new file mode 100644 index 0000000..c9a6296 --- /dev/null +++ b/app/(profile)/addfriend.tsx @@ -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 ( + + + + Add a Friend + + + + + {error ? ( + + {error} + + ) : null} + + + + + ); +}; + +export default AddFriendScreen; diff --git a/app/(profile)/friendrequests.tsx b/app/(profile)/friendrequests.tsx new file mode 100644 index 0000000..51db4f4 --- /dev/null +++ b/app/(profile)/friendrequests.tsx @@ -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 ( + + + + Pending Friend Requests + + + {error ? ( + + {error} + + ) : ( + + {pendingRequests.map((request) => ( + + {request.sender.username} + + + + ))} + + )} + + + ); +}; + +export default PendingFriendRequestsScreen; diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..14c981b --- /dev/null +++ b/app/(tabs)/_layout.tsx @@ -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 ( + + {/* Home Tab */} + , + headerRight: () => ( + !isLoggedIn ? ( + + + + + ) : ( + + ) + ), + }} + /> + + {/* Conditional Rendering for Messages Tab */} + {isLoggedIn ? ( + , + headerRight: () => ( + + + + + ) + }} + /> + ) : ( + , + }} + /> + )} + + {/* Conditional Rendering for Contacts Tab */} + {isLoggedIn ? ( + , + }} + /> + ) : ( + , + }} + /> + )} + + + ); +} diff --git a/app/(tabs)/contacts.tsx b/app/(tabs)/contacts.tsx new file mode 100644 index 0000000..ef53aa2 --- /dev/null +++ b/app/(tabs)/contacts.tsx @@ -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 ( + +
+ Your Contacts +
+ + {loading ? ( + Loading contacts... + ) : error ? ( + {error} + ) : contacts.length === 0 ? ( + No contacts found. Add some friends to start chatting! + ) : ( + contacts.map((contact) => ( + + {/* Profile Picture */} + + + + + {/* Contact Details */} + + + {contact.username} + + + {contact.status} {/* Placeholder for online/offline */} + + + + {/* Message Button */} + + + )) + )} +
+ ); +} diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx new file mode 100644 index 0000000..a235e89 --- /dev/null +++ b/app/(tabs)/index.tsx @@ -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 ( + + +

+ Welcome to a Privacy-Focused Messaging App +

+ + {/* Adds a subtle divider */} + + + 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. + + + + 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. + + + + 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. + + + + 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. + + + + + + {/* Footer always visible at the bottom */} + + By + + + Registering + + + You agree to + + terms and condidtions + + + + + Learn more by visiting our + + + Discord + + + + + + +
+ ); +} \ No newline at end of file diff --git a/app/(tabs)/messages.tsx b/app/(tabs)/messages.tsx new file mode 100644 index 0000000..a920916 --- /dev/null +++ b/app/(tabs)/messages.tsx @@ -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 Loading...; + } + + return ( + +
People You've Messaged
+ + {/* Check if there are no friends */} + {friends.length === 0 ? ( + + + You have no friends yet. Add some friends to start chatting! + + + ) : ( + // Check if there are no messages + messages.length === 0 ? ( + + + No messages yet. Start a conversation! + + + ) : ( + messages.map((message, index) => ( + + {/* Profile Picture */} + + + + + + {/* Username */} + + {message.username} + + + {/* Last message preview */} + + {message.lastMessage} + + + + {/* Timestamp */} + + + {new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + + {/* Button to open chat */} + + + )) + ) + )} +
+ ); +} diff --git a/app/+html.tsx b/app/+html.tsx new file mode 100644 index 0000000..db8f4b3 --- /dev/null +++ b/app/+html.tsx @@ -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 ( + + + + + + {/* + 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: + + */} + + {/* + 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. + */} + + + {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} +