diff --git a/client/src/components/Chat/ChatScreen.tsx b/client/src/components/Chat/ChatScreen.tsx index 12c9548e2..254358858 100644 --- a/client/src/components/Chat/ChatScreen.tsx +++ b/client/src/components/Chat/ChatScreen.tsx @@ -17,7 +17,7 @@ import MessageChannel from "../Common/MessageChannel"; import * as Crypto from "expo-crypto"; import { generateName } from "../../utils/scripts"; import { SignOutButton } from "../Common/AuthButtons" -import { MessageType } from "../../types/Message"; +import { Message } from "../../types/Message"; import { LocationProvider } from "../../contexts/LocationContext"; import { useSocket } from "../../contexts/SocketContext"; import { useSettings } from "../../contexts/SettingsContext"; @@ -38,42 +38,50 @@ const ChatScreen = () => { // Note: To prevent complexity, all user information is grabbed from different contexts and services. If we wanted most information inside of UserContext, we would have to import contexts within contexts and have state change as certain things mount, which could cause errors that are difficult to pinpoint. // Message loading and sending logic - const [messages, setMessages] = React.useState([]); + const [messages, setMessages] = React.useState([]); const [messageContent, setMessageContent] = React.useState(""); useEffect(() => { - if (socket === null) return // This line might need to be changed. - socket.on("message", (data: MessageType, ack) => { - console.log("Message recieved from server:", data); + if (socket === null) return; // This line might need to be changed + + const handleMessage = (data: any, ack?: any) => { + console.log("Message received from server:", data); + setMessages((prevMessages) => [...prevMessages, data]); if (ack) console.log("Server acknowledged message:", ack); - setMessages([...messages, data]) - }) + }; + + socket.on("message", handleMessage); + return () => { - socket.off() - } - }, [messages]) + socket.off("message", handleMessage); + }; + }, [messages, socket]); // For when the user sends a message (fired by the send button) const onHandleSubmit = () => { if (messageContent.trim() !== "") { - const newMessage: MessageType = { + const newMessage: Message = { author: { - uid: String(userAuth.userAuthInfo?.uid), + uid: String(userAuth.userAuthInfo?.uid), + displayName: "Anonymous", }, msgId: Crypto.randomUUID(), msgContent: messageContent.trim(), - timeSent: Date.now(), + timestamp: Date.now(), + lastUpdated: Date.now(), location: { lat: Number(location?.latitude), lon: Number(location?.longitude) - } + }, + isReply: false, + replyTo: "", + reactions: {}, } if (socket !== null) { socket.emit("message", newMessage) } - - setMessages([...messages, newMessage]); + setMessageContent(""); } }; diff --git a/client/src/components/Common/MessageChannel.tsx b/client/src/components/Common/MessageChannel.tsx index 03aaec1d0..25c28db30 100644 --- a/client/src/components/Common/MessageChannel.tsx +++ b/client/src/components/Common/MessageChannel.tsx @@ -17,8 +17,8 @@ const MessageChannel: React.FC = ({ messages }) => { renderItem={({ item }) => ( )} inverted={true} // This will render items from the bottom diff --git a/client/src/types/Message.ts b/client/src/types/Message.ts index 2310a2714..1e0f2fb50 100644 --- a/client/src/types/Message.ts +++ b/client/src/types/Message.ts @@ -1,14 +1,19 @@ -export interface MessageType { +export interface Message { author: { - uid: string - displayName?: string // To be only used for display purposes (do not send to server) + uid: string, + displayName: string, } msgId: string msgContent: string - timeSent: number // Unix timestamp; Date.now() returns a Number. + timestamp: number + lastUpdated: number location: { lat: number lon: number - geohash?: string + } + isReply: boolean + replyTo: string + reactions: { + [key: string]: number } } diff --git a/server/.gitignore b/server/.gitignore index 81da29c6f..1ed29ce47 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -133,7 +133,7 @@ dist build # Private Key JSON -./private_key/* +./firebase-secrets.json # Other .env diff --git a/server/src/actions/calculateDistance.ts b/server/src/actions/calculateDistance.ts new file mode 100644 index 000000000..ac863b120 --- /dev/null +++ b/server/src/actions/calculateDistance.ts @@ -0,0 +1,18 @@ +const degreesToRadians = (degrees: number) => { + return degrees * Math.PI / 180; +} + +export const calculateDistanceInMeters = (lat1: number, lon1: number, lat2: number, lon2: number) => { + const earthRadiusKm = 6371; + + const dLat = degreesToRadians(lat2-lat1); + const dLon = degreesToRadians(lon2-lon1); + + lat1 = degreesToRadians(lat1); + lat2 = degreesToRadians(lat2); + + const a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return earthRadiusKm * c * 1000; +} \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index 202a50f8b..63d3b360b 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -11,6 +11,8 @@ import {geohashForLocation} from 'geofire-common'; import { ConnectedUser } from './types/User'; import { getAuth } from 'firebase-admin/auth'; import Mailgun from "mailgun.js"; +import { messagesCollection } from './utilities/firebaseInit'; +import { calculateDistanceInMeters } from './actions/calculateDistance'; const { createServer } = require("http"); const { Server } = require("socket.io"); @@ -68,9 +70,35 @@ io.on("connection", async (socket: any) => { await createUser(defaultConnectedUser); await toggleUserConnectionStatus(socket.id); + const observer = messagesCollection.where("lastUpdated", ">", Date.now()).onSnapshot((querySnapshot) => { + querySnapshot.docChanges().forEach((change) => { + + if (change.type === "added"){ + console.log("New message: ", change.doc.data()); + + const messageLat = change.doc.data().location.lat; + const messageLon = change.doc.data().location.lon; + + const userLat = defaultConnectedUser.location.lat; + const userLon = defaultConnectedUser.location.lon; + + const distance = calculateDistanceInMeters(messageLat, messageLon, userLat, userLon); + + if (distance < 300) { + console.log("Message is within 300m of user"); + socket.emit("message", change.doc.data()); + } else { + console.log("Message is not within 300m of user"); + } + } + + }); + }); + socket.on("disconnect", () => { console.log(`[WS] User <${socket.id}> exited.`); deleteConnectedUserByUID(socket.id); + observer(); }); socket.on("ping", (ack) => { // The (ack) parameter stands for "acknowledgement." This function sends a message back to the originating socket. @@ -80,46 +108,9 @@ io.on("connection", async (socket: any) => { socket.on("message", async (message: Message, ack) => { // message post - when someone sends a message - console.log(`[WS] Recieved message from user <${socket.id}>.`); - console.log(message); try { - if (isNaN(message.timeSent)) - throw new Error("The timeSent parameter must be a valid number."); - if (isNaN(message.location.lat)) - throw new Error("The lat parameter must be a valid number."); - if (isNaN(message.location.lon)) - throw new Error("The lon parameter must be a valid number."); - - if ( - message.location.geohash == undefined || - message.location.geohash === "" - ) { - message.location.geohash = geohashForLocation([ - Number(message.location.lat), - Number(message.location.lon), - ]); - console.log(`New geohash generated: ${message.location.geohash}`); - } - - const status = await createMessage(message); - if (status === false) throw new Error("Error creating message: "); - - // Get nearby users and push the message to them. - const nearbyUserSockets = await findNearbyUsers( - Number(message.location.lat), - Number(message.location.lon), - Number(process.env.message_outreach_radius) - ); - for (const recievingSocket of nearbyUserSockets) { - // Don't send the message to the sender (who will be included in list of nearby users). - if (recievingSocket === socket.id) { - continue; - } else { - console.log(`Sending new message to socket ${recievingSocket}`); - socket.broadcast.to(recievingSocket).emit("message", message); - } - } - + const messageCreated = await createMessage(message); + if (!messageCreated) throw new Error("createMessage() failed."); if (ack) ack("message recieved"); } catch (error) { console.error("[WS] Error sending message:", error.message); @@ -130,6 +121,8 @@ io.on("connection", async (socket: any) => { try { const lat = Number(location.lat); const lon = Number(location.lon); + defaultConnectedUser.location.lat = lat; + defaultConnectedUser.location.lon = lon; const success = await updateUserLocation(socket.id, lat, lon); if (success) { console.log("[WS] Location updated in database successfully."); diff --git a/server/src/types/Message.ts b/server/src/types/Message.ts index 41b6e0966..1e0f2fb50 100644 --- a/server/src/types/Message.ts +++ b/server/src/types/Message.ts @@ -1,12 +1,19 @@ export interface Message { - uid: string + author: { + uid: string, + displayName: string, + } msgId: string msgContent: string - timeSent: number + timestamp: number + lastUpdated: number location: { lat: number lon: number - geohash?: string } - visibleToUids?: Array + isReply: boolean + replyTo: string + reactions: { + [key: string]: number + } } diff --git a/server/src/utilities/firebaseInit.ts b/server/src/utilities/firebaseInit.ts index beeb3f717..79bf6bee0 100644 --- a/server/src/utilities/firebaseInit.ts +++ b/server/src/utilities/firebaseInit.ts @@ -1,5 +1,5 @@ const admin = require('firebase-admin'); -const serviceAccount = require("../../.firebase-secrets.json"); +const serviceAccount = require("../../firebase-secrets.json"); export const adminApp = admin.initializeApp({ credential: admin.credential.cert(serviceAccount),