74 Commits

Author SHA1 Message Date
Nicholas Pease cf2fc469fb Fix Broken Friends on Map (#97) 2024-04-21 21:32:51 -04:00
npease 49bf406795 Fix Broken Friends on Map 2024-04-21 21:31:36 -04:00
Nicholas Pease d75496adcc Add Unit Tests (#95) 2024-04-21 21:15:23 -04:00
Nicholas Pease acfae6b898 Fix Markers on Empty Profiles (#96) 2024-04-21 20:59:48 -04:00
npease 373ff372cb Fix Markers on Empty Profiles 2024-04-21 20:58:08 -04:00
npease 6c54d38efc Add Unit Tests 2024-04-21 16:38:18 -04:00
Nicholas Pease df60173b49 Firebase Optimization (#93) 2024-04-21 13:58:49 -04:00
Nicholas Pease 3be86e6196 Proper Optimization for Firebase (#94) 2024-04-21 02:05:53 -04:00
npease 3d85f9770e Proper Optimization 2024-04-21 02:04:56 -04:00
npease 3af8e3bb6d Firebase Optimization 2024-04-20 23:42:44 -04:00
Nicholas Pease 6ad0f1ad55 Bugfix: My Rooms Loading (#92) 2024-04-19 17:34:59 -04:00
Nicholas Pease 6fb88acf44 Merge branch 'main' into npease-bugfix-room-load 2024-04-19 11:34:33 -04:00
npease d98de59fcf Bugfix: My Rooms Loading 2024-04-19 11:33:57 -04:00
Nicholas Pease ca691d2203 DM Security Fix (#91) 2024-04-19 11:29:21 -04:00
Nicholas Pease 31842373b8 Multiple Mapping Improvements (#90) 2024-04-19 11:25:32 -04:00
npease fcdc95505e Straggler 2024-04-19 11:24:34 -04:00
Nicholas Pease af55b7e58d Merge branch 'main' into npease-mapping-improvements 2024-04-19 11:21:19 -04:00
npease 094cdab65e DM Security Fix 2024-04-19 11:18:46 -04:00
Nicholas Pease 059a90808a Add Search to Nearby Tab (#89) 2024-04-19 11:13:03 -04:00
Nicholas Pease ea6801fbb5 Fix Image Detection Async (#88) 2024-04-19 11:12:43 -04:00
npease 925c6f28fe Map Improvements 2024-04-19 11:09:12 -04:00
npease 75a3919eb2 Added Friends, My Rooms, Refactored Nearby Markers Functions 2024-04-19 10:01:04 -04:00
npease 803432db8e Linting Fixes 2024-04-19 09:13:41 -04:00
npease b8a732eea5 Add Nearby Rooms Search 2024-04-19 09:08:48 -04:00
npease 294dd3a6b0 Image Fix Async 2024-04-19 02:32:18 -04:00
Nicholas Pease de0c9e9785 Circle Icon Fix (#87) 2024-04-19 02:09:09 -04:00
npease 45f9ec19d1 Circle Icon Fix 2024-04-19 02:08:03 -04:00
Nicholas Pease f73cd7f07a Better Picture Detection / Handling (#86) 2024-04-19 02:01:55 -04:00
Nicholas Pease fb19f53d63 Fix Delete (#85) 2024-04-19 02:01:42 -04:00
npease f1e23ac056 Better Picture Detection / Handling 2024-04-19 01:59:43 -04:00
npease 50308ce43c Fix Delete 2024-04-19 00:09:51 -04:00
Nicholas Pease d7d47bae9e Fix disappearing rooms on status visibility change (#84) 2024-04-18 23:49:26 -04:00
npease e203a87a77 Fix disappearing rooms on status visibility change 2024-04-18 23:29:47 -04:00
npease 5f089341be Clear Error From Console 2024-04-18 23:24:57 -04:00
Nicholas Pease f2c9d55a20 General Bug Fixes (#83) 2024-04-18 23:17:04 -04:00
npease 04c0cb4cff Deleted Key Variable 2024-04-18 23:15:26 -04:00
npease 32dda6fe8d Cleanup Console.Logs 2024-04-18 23:14:33 -04:00
Nicholas Pease da7320255e Fix Message Parsing Issues (#82) 2024-04-18 23:12:31 -04:00
npease e8fad393f7 Fix Message Parsing Issues 2024-04-18 23:11:38 -04:00
Nicholas Pease 02a9eeaa36 Add User Message Deletion (#78) 2024-04-18 22:39:55 -04:00
Nicholas Pease c4374da5ba Merge branch 'main' into npease-chat-message-deletion 2024-04-18 22:39:14 -04:00
Nicholas Pease 2a33838b93 Fix Profile Picture Display (#81) 2024-04-18 14:41:42 -04:00
Nicholas Pease 527dcd8660 Properly Integrate Images, RMF and Profane filters (#80) 2024-04-18 14:40:31 -04:00
npease d1a9116b34 Fix Profile Picture Scaling on Different Screen Sizes 2024-04-18 13:01:41 -04:00
npease 9ee81aa487 Profile Picture Sizing Corrected on Sidebar 2024-04-18 12:59:12 -04:00
npease de99e26ca2 Properly Integrate Images, RMF and Profane filters 2024-04-18 12:47:02 -04:00
Nicholas Pease 2822152799 Add Image Detection to RMF (Rich Message Formatting) (#79) 2024-04-18 01:19:47 -04:00
Nicholas Pease baa754eeb8 Add Profanity Filter (#77) 2024-04-18 01:18:36 -04:00
Nicholas Pease fe90a39983 Add Global User Online / Offline (#76) 2024-04-18 01:18:16 -04:00
npease 5eb2002d8e Install in proper directory 2024-04-18 00:07:29 -04:00
npease 3446ea47b4 Add dependency to package.json 2024-04-18 00:05:43 -04:00
npease 32a5f5b057 Add Image Detection to RMF (Rich Message Formatting) 2024-04-18 00:01:17 -04:00
npease 8f00bade79 Add message deletion for users messages only 2024-04-17 23:24:08 -04:00
npease c3d55c94d1 Add Profanity Filter 2024-04-17 22:46:55 -04:00
npease 7c00b49870 Simple Fix for Blank Messages 2024-04-17 22:27:25 -04:00
npease 5b161ad232 Add Profile Menu Option to Appear Offline 2024-04-17 22:18:20 -04:00
npease 7954432add Add Global User Online Status 2024-04-17 22:06:24 -04:00
Sgoodridge96 76f18d2b15 Sgoodridge (#75) 2024-04-11 01:46:29 -04:00
Stephen a695a47d8b Merge branch 'main' into sgoodridge 2024-04-11 01:32:03 -04:00
Stephen 401cc4670c Fix profile pic bugs 2024-04-11 01:29:12 -04:00
Nicholas Pease 0668350eed Add Privacy Policy (For Google App Store) (#74) 2024-04-10 21:33:53 -04:00
npease ad39b54f1d Add Deletion Info Page 2024-04-10 14:14:20 -04:00
npease 398725e7b7 Add Privacy Police (For Google App Store) 2024-04-10 14:07:31 -04:00
ClarkLach f726d6d40f Map Marker Tweaks (#73) 2024-04-09 21:47:03 -04:00
ClarkLach 1e3c0ce930 Custom Icons POC
Will need to pick out some good icons/styles. Overlay offset needs some work still.
2024-04-09 20:33:49 -04:00
ClarkLach 0eb766b2a0 Hover effect for map rooms
Hopefully nothing is broken, some formatting still needed
2024-04-09 15:46:19 -04:00
Nicholas Pease 6c9f4af2dd Various Bug Fixes (#72) 2024-04-08 10:26:52 -04:00
npease ed6c1b427e Bug Fix - JSDoc Build Error 2024-04-08 00:13:14 -04:00
npease bc25b0e694 Bug Fix - Unauth Redirect 2024-04-08 00:08:21 -04:00
npease 3e354800dd Bug Fix - DM List 2024-04-07 23:59:29 -04:00
npease c3c76b66c9 Bug Fix - Restore Undid Change 2024-04-07 23:45:12 -04:00
npease ce5add2879 Bug Fix - Build Error 2024-04-07 23:33:27 -04:00
npease 5629dc8836 Bug Fix - Profile Editing 2024-04-07 23:31:18 -04:00
npease 0d4787ccfb Bug Fix - Profile Loading 2024-04-07 22:37:35 -04:00
34 changed files with 5906 additions and 308 deletions
+4 -6
View File
@@ -1,9 +1,9 @@
![](/frontend-next/public/logos/logo_transparent.png)
Main repo for ChatMaps, our COS420 Project.
ChatMaps is a web-based social networking service that allows users to connect to others in their local geographic area. It implements an interactable mapping utility to show general user locations relative to other users, as well as a chat room feature that allows users to start public conversations based on a specified topic. ChatMaps is primarily intended for use in densely populated areas, such as college campuses or metropolitan areas, so people of similar interests can start conversations. The goal of this project is to create a web app that plots locations, gives a radius of the local area, and connects users into different topic-based chat rooms.
ChatMaps is a web-based social networking service that allows users to connect to others in their local geographic area. It implements an interactable mapping utility to show general user locations relative to others, as well as a chat room feature that allows users to start public conversations based on any given topic. ChatMaps is primarily intended for use in densely populated areas, such as college campuses or metropolitan areas, so people of similar interests can start new conversations. The goal of this project is to create a web app that plots locations, gives a radius of the local area, and connects users into different topic-based chat rooms.
This service implements user login and profiles, allowing users to add each other as friends and start private conversations. There are several default chat rooms of varying topics, but users also have the ability to create their own topics that will be viewable by other users. For example, a user at the University of Maine could create a joinable chat room titled “COS420”, which would be visible to others near this campus.
This service implements user login and profiles, allowing users to add each other as friends and start private conversations. There are several default chat rooms of varying topics, but users also have the ability to create their own rooms that will be visible to other users. For example, a user at the University of Maine could create a joinable chat room titled “COS420”, which would be joinable by others near this campus.
This app shares some similarities to other social networks that implement location-based content. ChatMaps novel approach is to utilize user location to facilitate real-time communication with others within a given radius.
@@ -11,15 +11,13 @@ The live version of this app can be found at:
https://chatma.ps/
A local version can be run with:
A local version can be run (assuming you have Node installed) with:
cd frontend-next/
npm install
npm run build
npm run start
npm run dev
then navigating to:
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": ["next/babel","next/core-web-vitals" ],
"extends": ["next/core-web-vitals"],
"rules": {
"no-unused-vars": ["warn", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }],
"jsx-a11y/alt-text": "off",
+22
View File
@@ -0,0 +1,22 @@
import { Chat } from "../src/components/app/datatypes";
import renderer from 'react-test-renderer';
var exampleChatObj = {
body: "Hello, World!",
isSystem: false,
timestamp: 1710527946340,
user: "TestUser"
}
var exampleUser = {
uid: "123456",
username: "TestUser",
lastOnline: true
}
it('Chat Renders Correctly', () => {
const tree = renderer
.create((<Chat chatObj={exampleChatObj} user={exampleUser} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
@@ -0,0 +1,14 @@
import { ChatRoomSidebar } from "../src/components/app/datatypes";
import renderer from 'react-test-renderer';
var exampleRoom = {
name: "TestRoom",
description: "This is a test room.",
}
it('ChatRoomSidebar Renders Correctly', () => {
const tree = renderer
.create((<ChatRoomSidebar roomObj={exampleRoom} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
+30
View File
@@ -0,0 +1,30 @@
import { DM } from "../src/components/app/friends/dm";
import renderer from 'react-test-renderer';
var message = "Hello, World! This is a test message. https://www.google.com"
var exampleUser = {
uid: "123456",
username: "TestUser",
firstName: "Test",
lastName: "User",
lastOnline: true,
pfp: "https://th.bing.com/th/id/OIP.K5VoTfw97JiEc1OBODAjmQHaHO?rs=1&pid=ImgDetMain"
}
var friendObj = {
uid: "123456",
username: "TestUser",
firstName: "Test",
lastName: "Friend",
lastOnline: true,
pfp: "https://th.bing.com/th/id/OIP.K5VoTfw97JiEc1OBODAjmQHaHO?rs=1&pid=ImgDetMain"
}
it('RMF Renders Correctly', () => {
const tree = renderer
.create(<DM user={exampleUser} friendObj={friendObj} />)
.toJSON();
expect(tree).toMatchSnapshot();
});
+25
View File
@@ -0,0 +1,25 @@
import { FirstPage } from "@mui/icons-material";
import { Member } from "../src/components/app/datatypes";
import renderer from 'react-test-renderer';
var exampleChatObj = {
body: "Hello, World!",
isSystem: false,
timestamp: 1710527946340,
user: "TestUser"
}
var exampleUser = {
uid: "123456",
username: "TestUser",
firstName: "Test",
lastName: "User",
lastOnline: true
}
it('Member Renders Correctly', () => {
const tree = renderer
.create((<Member memberObj={exampleUser} />))
.toJSON();
expect(tree).toMatchSnapshot();
});
+11
View File
@@ -0,0 +1,11 @@
import { RMF } from "../src/components/app/datatypes";
import renderer from 'react-test-renderer';
var message = "Hello, World! This is a test message. https://www.google.com"
it('RMF Renders Correctly', () => {
const tree = renderer
.create((RMF(message)))
.toJSON();
expect(tree).toMatchSnapshot();
});
@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Chat Renders Correctly 1`] = `
<div
className="width-[100%] bg-white rounded-lg mt-1 text-left p-1 grid grid-cols-2 mr-2"
>
<div>
<span
className="mr-[5px]"
style={
{
"color": "#133337",
}
}
>
<a
className="hover:font-bold cursor-pointer"
href="/user?uid=undefined"
onClick={[Function]}
onMouseEnter={[Function]}
onTouchStart={[Function]}
>
TestUser
</a>
</span>
Hello, World!
</div>
<div
className="text-right text-[#d1d1d1]"
>
3/15/2024, 2:39:06 PM
</div>
</div>
`;
@@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChatRoomSidebar Renders Correctly 1`] = `
<div
className="border-[black] border-1 shadow-lg p-2 m-2 rounded-lg cursor-pointer"
>
<a
href="/chat?room=undefined/TestRoom-undefined"
onClick={[Function]}
onMouseEnter={[Function]}
onTouchStart={[Function]}
>
<div
className="grid grid-cols-3"
>
<div
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
data-testid="PersonIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4m0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4"
/>
</svg>
<div>
0
/
0
</div>
</div>
<div
className="col-span-2"
>
<div
className="font-bold"
>
TestRoom
</div>
<div
className="italic"
>
This is a test room.
</div>
</div>
</div>
</a>
</div>
`;
@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RMF Renders Correctly 1`] = `
<div
className="border-[black] border-1 shadow-lg m-2 rounded-lg"
>
<div
className="grid grid-cols-4"
>
<div
className="place-content-center"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium cursor-pointer css-i4bv87-MuiSvgIcon-root"
data-testid="ChatIcon"
focusable="false"
onClick={[Function]}
viewBox="0 0 24 24"
>
<path
d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2M6 9h12v2H6zm8 5H6v-2h8zm4-6H6V6h12z"
/>
</svg>
</div>
<div
className="col-span-3 cursor-pointer"
>
<div
onClick={[Function]}
>
<div
className="grid grid-cols-2 justify-items-center"
>
<div
className="mr-8"
>
<img
className="w-[50px] h-[50px]"
src="https://th.bing.com/th/id/OIP.K5VoTfw97JiEc1OBODAjmQHaHO?rs=1&pid=ImgDetMain"
/>
</div>
<div
className=""
>
<div
className="font-bold"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSize20px text-lime-600 mr-1 relative top-[-2px] css-821wyw-MuiSvgIcon-root"
data-testid="CircleIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2"
/>
</svg>
Test
Friend
</div>
<div
className=""
>
@
TestUser
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Member Renders Correctly 1`] = `
<a
href="/user?uid=123456"
onClick={[Function]}
onMouseEnter={[Function]}
onTouchStart={[Function]}
>
<div
className="cursor-pointer g-[aliceblue] rounded-lg m-3 shadow-xl p-2"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root MuiSvgIcon-fontSize20px text-lime-600 mr-1 relative top-[-1px] css-821wyw-MuiSvgIcon-root"
data-testid="CircleIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2"
/>
</svg>
TestUser
</div>
</a>
`;
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RMF Renders Correctly 1`] = `
<span
className="mr-2"
>
Hello, World! This is a test message.
<img
className="max-w-[100%]"
src="https://www.google.com"
/>
</span>
`;
+204
View File
@@ -0,0 +1,204 @@
const nextJest = require('next/jest')
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
/** @type {import('jest').Config} */
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
})
const config = {
testEnvironment: 'jsdom',
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "C:\\Users\\Nick\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\",
// "\\.pnp\\.[^\\\\]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
module.exports = createJestConfig(config)
+4722 -17
View File
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -6,9 +6,13 @@
"dev": "next dev",
"build": "next build",
"start": "npx serve@latest out",
"lint": "next lint"
"lint": "next lint",
"test": "jest"
},
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/preset-env": "^7.24.4",
"@babel/preset-react": "^7.24.1",
"@capacitor/android": "^5.7.4",
"@capacitor/core": "^5.7.4",
"@capacitor/geolocation": "^5.0.7",
@@ -17,6 +21,7 @@
"@headlessui/react": "^1.7.18",
"@mui/icons-material": "^5.15.14",
"@mui/material": "^5.15.14",
"bad-words": "^3.0.4",
"firebase": "^10.6.0",
"next": "^14.1.0",
"pigeon-maps": "^0.21.3",
@@ -24,14 +29,19 @@
"react-beforeunload": "^2.6.0",
"react-dom": "^18.2.0",
"react-firebase-hooks": "^5.1.1",
"react-hook-form": "^7.50.1"
"react-hook-form": "^7.50.1",
"react-test-renderer": "^18.2.0"
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "^5.7.4",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^15.0.2",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8",
"tailwindcss": "^3.3.0"
}
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>User Data Deletion Request</title>
</head>
<body>
<h1>User Data Deletion Request</h1>
<p>
To request the deletion of your user data from our system, please follow the steps below:
</p>
<ol>
<li>Compose an email to <a href="mailto:deletion@chatma.ps">deletion@chatma.ps</a>.</li>
<li>In the email subject, write "User Data Deletion Request".</li>
<li>In the body of the email, provide the following information:</li>
<ul>
<li>Your username: [Insert your username here]</li>
<li>Your email address associated with the account: [Insert your email address here]</li>
</ul>
<li>Send the email.</li>
</ol>
<p>
Our team will process your request as soon as possible. Please note that the deletion of your user data may take some time, and we will notify you once it has been completed.
</p>
<p>
If you have any further questions or concerns, please don't hesitate to contact us at <a href="mailto:support@chatma.ps">support@chatma.ps</a>.
</p>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChatMaps Privacy Policy</title>
</head>
<body>
<h1>ChatMaps Privacy Policy</h1>
<p>At ChatMaps, we are committed to protecting your privacy and ensuring the security of your personal information. This privacy policy explains how we collect, use, and safeguard your data when you use our application.</p>
<h2>Collection of Personal Information</h2>
<p>ChatMaps requires access to your location in order to provide you with location-based services. However, we do not share, sell, or disclose your personal data to any third party.</p>
<h2>Child Safety</h2>
<p>ChatMaps is fully compliant with online child safety laws. We do not knowingly collect personal information from children under the age of 13. If you believe that we have inadvertently collected personal information from a child, please contact us immediately so that we can take appropriate action.</p>
<h2>Data Security</h2>
<p>We take the security of your personal information seriously. We implement industry-standard security measures to protect your data from unauthorized access, alteration, or disclosure.</p>
<h2>Changes to this Privacy Policy</h2>
<p>We reserve the right to update or modify this privacy policy at any time. Any changes will be effective immediately upon posting the updated policy on our website.</p>
<h2>Contact Us</h2>
<p>If you have any questions or concerns about our privacy policy, please contact us at privacy@chatma.ps .</p>
</body>
</html>
+17 -5
View File
@@ -7,7 +7,7 @@ import Drawer from '@mui/material/Drawer';
// Firebase Imports
import { auth, database } from "../../../firebase-config";
import { ref, onValue } from "firebase/database";
import { ref, onValue, set } from "firebase/database";
import { useAuthState } from "react-firebase-hooks/auth"
// Component Imports
@@ -27,7 +27,7 @@ function Home() {
// State variables for app page
const [user, setUser] = useState(null); // user data
const [loadingLoc, setLoadingLoc] = useState(true); // location variable loading, true = loading, false = finished loading
const [authUser] = useAuthState(auth) // auth user object (used to obtain other user object)
const [authUser, authLoading] = useAuthState(auth) // auth user object (used to obtain other user object)
const [drawerOpen, setDrawerOpen] = useState(true); // drawer open state
const [coords, setCoords] = useState(null)
@@ -42,17 +42,19 @@ function Home() {
// Authentication Verification / Redirection if Profile Data not Filled out
useEffect(() => {
if (authUser) {
if (authUser && authLoading === false) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser(userData);
setUser({...userData});
} else {
window.location.href = "/onboarding";
}
});
} else if (authLoading === false) {
window.location.href = "/login";
}
}, [authUser])
}, [authLoading])
useEffect(() => {
Geolocation.getCurrentPosition().then((position) => {
@@ -61,6 +63,16 @@ function Home() {
});
}, [])
// Saves users last loc to profile for friends map
useEffect(() => {
if (coords && user) {
set(ref(database,`users/${user.uid}/location`), {
latitude: coords.latitude,
longitude: coords.longitude
})
}
}, [coords, user])
return (
<div className="overflow-hidden h-dvh">
{user && (
+10 -5
View File
@@ -5,7 +5,7 @@ import { useState, useEffect } from "react";
// Firebase Imports
import { auth, database } from "../../../firebase-config";
import { ref, onValue, set, onDisconnect } from "firebase/database";
import { ref, onValue, set, onDisconnect, get, onChildAdded, onChildRemoved } from "firebase/database";
import { useAuthState } from "react-firebase-hooks/auth"
// Component Imports
@@ -24,7 +24,7 @@ function Chat() {
const [user, setUser] = useState(null); // user data
const [chatRoomObj, setChatRoomObj] = useState(null); // Current chatroom object
const [doneLoading, setDoneLoading] = useState(false) // is the page done loading or not
const [authUser] = useAuthState(auth) // auth user object (used to obtain other user object)
const [authUser, authLoading] = useAuthState(auth) // auth user object (used to obtain other user object)
const [drawerOpen, setDrawerOpen] = useState(true); // drawer open state
var windowSize = useWindowSize()
@@ -38,7 +38,7 @@ function Chat() {
// Authentication Verification / Redirection if Profile Data not Filled out
useEffect(() => {
if (authUser) {
if (authUser && authLoading === false) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
@@ -47,8 +47,10 @@ function Chat() {
window.location.href = "/onboarding";
}
});
} else if (authLoading === false) {
window.location.href = "/login";
}
}, [authUser])
}, [authLoading])
// Users URL params to load proper chatroom, then logs the user into that room
useEffect(() => {
@@ -88,13 +90,16 @@ function Chat() {
uid: user.uid,
})*/
onValue(ref(database, `/rooms/${path}`), (roomData) => {
// Room Object Load
get(ref(database, `/rooms/${path}`)).then((roomData) => {
roomData = roomData.val();
setChatRoomObj(roomData)
if (!doneLoading) {
setDoneLoading(true)
}
})
}
}, [user]);
+27 -20
View File
@@ -5,7 +5,7 @@ import { useState, useEffect } from "react";
// Firebase Imports
import { auth, database } from "../../../firebase-config";
import { ref, onValue, set, onDisconnect } from "firebase/database";
import { ref, onValue, set, onDisconnect, get, onChildAdded, onChildRemoved} from "firebase/database";
import { useAuthState } from "react-firebase-hooks/auth"
// Component Imports
@@ -24,8 +24,9 @@ function Chat() {
const [user, setUser] = useState(null); // user data
const [chatRoomObj, setChatRoomObj] = useState(null); // Current chatroom object
const [doneLoading, setDoneLoading] = useState(false) // is the page done loading or not
const [authUser] = useAuthState(auth) // auth user object (used to obtain other user object)
const [authUser, authLoading] = useAuthState(auth) // auth user object (used to obtain other user object)
const [drawerOpen, setDrawerOpen] = useState(true); // drawer open state
const [isUserAuthed, setIsUserAuthed] = useState(false); // is the user authenticated or not
var windowSize = useWindowSize()
useEffect(() => {
@@ -38,24 +39,29 @@ function Chat() {
// Authentication Verification / Redirection if Profile Data not Filled out
useEffect(() => {
if (authUser) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser(userData);
} else {
window.location.href = "/onboarding";
}
});
if (authUser && authLoading === false) {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
setUser(userData);
} else {
window.location.href = "/onboarding";
}
});
} else if (authLoading === false) {
window.location.href = "/login";
}
}, [authUser])
}, [authLoading])
// Users URL params to load proper chatroom, then logs the user into that room
useEffect(() => {
if (user) {
const searchParams = new URLSearchParams(document.location.search);
var path = searchParams.get("dm")
if (path.includes(user.uid))
setIsUserAuthed(true)
else
location.href = "/app"
/*// Send entered message
var payload = {
body: "entered",
@@ -88,19 +94,20 @@ function Chat() {
uid: user.uid,
})*/
onValue(ref(database, `/dms/${path}`), (roomData) => {
roomData = roomData.val();
setChatRoomObj(roomData)
if (!doneLoading) {
setDoneLoading(true)
}
// Room Object Load
get(ref(database, `/dms/${path}`)).then((roomData) => {
roomData = roomData.val();
setChatRoomObj(roomData)
if (!doneLoading) {
setDoneLoading(true)
}
})
}
}, [user]);
return (
<div>
{(authUser && doneLoading) && (
{(authUser && doneLoading && isUserAuthed) && (
<div className="overflow-hidden h-dvh">
{/* Left Side of Page */}
<div className="overflow-hidden h-dvh md:mr-[400px]">
+20 -15
View File
@@ -3,7 +3,8 @@
import { useState, useEffect } from "react";
import { auth, database } from "../../../firebase-config";
import { ref, onValue, get } from "firebase/database";
import { onAuthStateChanged } from "firebase/auth";
import { useAuthState } from "react-firebase-hooks/auth"
// Refactored Component Imports
@@ -18,6 +19,9 @@ import { Header } from "../../components/app/header";
// Friend Import
import { addFriend } from "../../components/app/friends/friends";
// Icons
import CircleIcon from '@mui/icons-material/Circle';
/**
* User Profile Page
* @returns {Object} - User Profile Page
@@ -31,6 +35,8 @@ function UserProfile() {
const [isOwner, setIsOwner] = useState(false); // Determines if user is owner of profile
const [friends, setFriends] = useState(false); // is user a friend?
const [isPending, setPending] = useState(false); // is friend request pending?
const [authUser, authLoading] = useAuthState(auth) // auth user object (used to obtain other user object)
// Handles Edit State in Component, shares useState with ProfileEdit
const [isEditing, setIsEditing] = useState(false);
@@ -40,25 +46,25 @@ function UserProfile() {
// Authentication
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (authUser && authLoading === false) {
const searchParams = new URLSearchParams(document.location.search);
var userUID = searchParams.get("uid")
if (user) {
get(ref(database, `users/${user.uid}`)).then((userData) => {
onValue(ref(database, `users/${authUser.uid}`), (userData) => {
userData = userData.val();
if (userData) {
if (userData.uid == userUID) {
setIsOwner(true);
}
setUser(userData);
setIsAuthenticated(true);
setUser(userData);
} else {
window.location.href = "/onboarding";
window.location.href = "/onboarding";
}
});
}
});
}, []);
});
} else if (authLoading === false) {
window.location.href = "/login";
}
}, [authLoading]);
// Grabs profile user data
useEffect(() => {
@@ -108,7 +114,7 @@ function UserProfile() {
return (
<div>
{isAuthenticated && (
{(isAuthenticated && profileData) && (
<div className="md:overflow-hidden">
{/* Left Side of Page */}
<div className="h-dvh md:overflow-hidden">
@@ -121,11 +127,10 @@ function UserProfile() {
<div>
<img
src={profileData.pfp}
width="300px"
className="relative mx-auto rounded-2xl overflow-hidden"
className="relative mx-auto rounded-2xl overflow-hidden w-[90%]"
/>
<div className="font-bold text-[30px]">
{profileData.firstName} {profileData.lastName}
<div className="font-bold text-[30px] flex justify-center items-center">
{profileData.lastOnline == true && <CircleIcon className="text-lime-600 mr-3"/>}{profileData.firstName} {profileData.lastName}
</div>
<div className="text-[20px]">@{profileData.username}</div>
<div className="pt-5">{profileData.bio}</div>
+58 -12
View File
@@ -1,8 +1,15 @@
import Link from "next/link"
import { useEffect, useState } from "react";
const Filter = require('bad-words')
const filter = new Filter();
import {database} from "../../../firebase-config"
import {remove, ref} from "firebase/database"
// Icons
import PersonIcon from '@mui/icons-material/Person';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import CircleIcon from '@mui/icons-material/Circle';
// Colors for Messages
const userColors = [
@@ -30,25 +37,54 @@ let dateOptions = {
minute: "2-digit",
};
/**
* Photo URL Test
* @param {String} url - URL to Test
* @returns {Boolean} - Image Loaded (True) or Not (False)
*/
function imageProcessing(url) {
var x = async () => {
var img = new Image();
img.src = url;
img.onload = function() {
return true;
}
img.onerror = function() {
return false;
}
}
var res = x()
return res
}
/**
* Rich Message Formatting
* @param {String} message - Message to Format
* @returns {String} - Formatted Message (IN HTML)
*/
function RMF(message) {
export function RMF(message) {
var URLREGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
var URLmatch = message.match(URLREGEX);
var newMessage = URLmatch ? [] : message
if (URLmatch) {
for (var i = 0; i < URLmatch.length; i++) {
var link = (<span>
{message.split(URLmatch[i])[0]}
if (imageProcessing("https://"+URLmatch[i])) {
newMessage.push((<span className="mr-2">
{(URLmatch.length == 1) && message.split(URLmatch[i])[0].replace("https://","").replace("http://","")}
<img src={"https://"+URLmatch[i]} className="max-w-[100%]"/>
{(i == URLmatch.length || URLmatch.length == 1) && message.split(URLmatch[i])[1]}
</span>))
} else {
newMessage.push((<span className="mr-2">
{URLmatch.length == 1 && message.split(URLmatch[i])[0]}
<Link href={"https://"+URLmatch[i]} target="_blank" className="hover:underline">{URLmatch[i]}</Link>
{message.split(URLmatch[i])[1]}
</span>)
message = link
{(i == URLmatch.length || URLmatch.length == 1) && message.split(URLmatch[i])[1]}
</span>))
}
}
}
return message
return newMessage
}
/**
* Grabs Window Size
@@ -95,18 +131,28 @@ const generateColor = (user_str) => {
* @props {JSON} chatObj - Chat Object
* @returns {Object} - Chat Message Component
*/
export function Chat({ chatObj }) {
var message = RMF(chatObj.body)
export function Chat({ chatObj, user, path }) {
function deleteMessage() {
remove(ref(database, `${path}/chats/${chatObj.timestamp}-${chatObj.user}`))
}
var messageFilterBypass = [undefined, null, '', ' ', '\'', '\"']
if (!messageFilterBypass.includes(chatObj.body) && (chatObj.body.length != 1 && !chatObj.body[0].match(/\W/))) {
var message = filter.clean(chatObj.body)
message = RMF(message)
} else {
var message = chatObj.body
}
return (
<div className="width-[100%] bg-white rounded-lg mt-1 text-left p-1 grid grid-cols-2 mr-2">
<div>
<span style={{ color: userColors[generateColor(chatObj.user)] }}>
{user.uid == chatObj.uid && <DeleteOutlineIcon fontSize="" className="ml-1 mr-1 cursor-pointer" onClick={() => {deleteMessage()}}/>}
<span className="mr-[5px]" style={{ color: userColors[generateColor(chatObj.user)] }}>
<Link href={`/user?uid=${chatObj.uid}`}
className="hover:font-bold cursor-pointer">
{chatObj.user}
</Link>
</span>
: {message}
{message}
</div>
<div className="text-right text-[#d1d1d1]">
{new Date(chatObj.timestamp).toLocaleString(dateOptions)}
@@ -149,7 +195,7 @@ export function Member({ memberObj }) {
return (
<Link href={"/user?uid=" + memberObj.uid}>
<div className="cursor-pointer g-[aliceblue] rounded-lg m-3 shadow-xl p-2">
{memberObj.username}
{memberObj.lastOnline == true && <CircleIcon className="text-lime-600 mr-1 relative top-[-1px]" fontSize="20px"/>}{memberObj.username}
</div>
</Link>
);
@@ -3,7 +3,7 @@ import { database } from "../../../../firebase-config"
import { ref, set, get } from "firebase/database";
import ChatIcon from '@mui/icons-material/Chat';
import CircleIcon from '@mui/icons-material/Circle';
export function openDM(user, uid) {
var uid1 = user.uid < uid? user.uid : uid
@@ -44,10 +44,12 @@ export function DM({user,friendObj}) {
</div>
<div className='col-span-3 cursor-pointer'>
<div onClick={() => {openDM(user,friendObj.uid)}}>
<div className='inline-block mr-5'><img src={friendObj.pfp} className='w-[50px]'/></div>
<div className='inline-block relative top-[-6px]'>
<div className="font-bold">{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
<div className='grid grid-cols-2 justify-items-center'>
<div className='mr-8'><img src={friendObj.pfp} className= 'w-[50px] h-[50px]'/></div>
<div className=''>
<div className="font-bold">{friendObj.lastOnline == true && <CircleIcon className="text-lime-600 mr-1 relative top-[-2px]" fontSize="20px"/>}{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
</div>
</div>
</div>
</div>
@@ -12,6 +12,7 @@ import { openDM } from './dm';
// Icons
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import CircleIcon from '@mui/icons-material/Circle';
/**
* Send a friend request to a user
@@ -39,10 +40,12 @@ export function Friend({user,friendObj}) {
</div>
<div className='col-span-3 cursor-pointer'>
<Link href={`/user?uid=${friendObj.uid}`}>
<div className='inline-block mr-5'><img src={friendObj.pfp} className='w-[50px]'/></div>
<div className='inline-block relative top-[-6px]'>
<div className="font-bold">{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
<div className='grid grid-cols-2 justify-items-center'>
<div className='mr-8'><img src={friendObj.pfp} className= 'w-[50px] h-[50px]'/></div>
<div className=''>
<div className="font-bold">{friendObj.lastOnline == true && <CircleIcon className="text-lime-600 mr-1 relative top-[-2px]" fontSize="20px"/>}{friendObj.firstName} {friendObj.lastName}</div>
<div className="">@{friendObj.username}</div>
</div>
</div>
</Link>
</div>
@@ -55,7 +58,7 @@ export function Friend({user,friendObj}) {
*
* @prop {JSON} user - User Object
* @prop {JSON} requestingUser - User Object of the user requesting to be friends
* @returns
* @returns {Object} Friend Request Component
*/
export function FriendRequest({user, requestingUser}) {
/**
@@ -2,7 +2,7 @@
import { Form, useForm } from "react-hook-form";
// Firebase Imports
import { ref, set } from "firebase/database";
import { ref, set, onChildAdded, onChildRemoved } from "firebase/database";
import { database } from "../../../../firebase-config";
// Component Imports
@@ -11,6 +11,11 @@ import { Chat, SystemMessage } from "../datatypes";
// Icons
import SendIcon from '@mui/icons-material/Send';
// Notification
import { createNotification } from "../notifications/notifications";
import { useEffect, useState } from "react";
/**
* Chat Room Component
* @prop {JSON} roomObj - Room Object
@@ -18,29 +23,61 @@ import SendIcon from '@mui/icons-material/Send';
* @returns {Object} - Chat Room Component
*/
export function DMRoom({ roomObj, user }) {
const [chatRoomObj, setChatRoomObj] = useState(roomObj);
const [chats, setChats] = useState(null);
var { register, control, reset, handleSubmit } = useForm();
// Message updater
var chatsArr = [];
var messages = roomObj.chats;
for (var message in messages) {
if (messages[message].isSystem) {
chatsArr.push(
<SystemMessage
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
} else {
chatsArr.push(
<Chat
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
// Listeners for DMs
useEffect(() => {
var path = roomObj.UIDs[0] < roomObj.UIDs[1] ? roomObj.UIDs[0] + "-" + roomObj.UIDs[1] : roomObj.UIDs[1] + "-" + roomObj.UIDs[0];
onChildAdded(ref(database, `/dms/${path}/chats`), (newDM) => {
if (chatRoomObj) {
var newDMRoomObj = chatRoomObj
if (newDMRoomObj) {
if (!newDMRoomObj.chats) {
newDMRoomObj.chats = {}
}
newDMRoomObj.chats[newDM.key] = newDM.val()
setChatRoomObj({...newDMRoomObj})
}
}
});
onChildRemoved(ref(database, `/dms/${path}/chats`), (removed) => {
if (chatRoomObj) {
var newDMRoomObj = chatRoomObj
var deleted = removed.val()
delete newDMRoomObj.chats[`${deleted.timestamp}-${deleted.user}`]
setChatRoomObj({...newDMRoomObj})
}
});
}, [])
useEffect(() => {
// Message updater
var chatsArr = [];
var messages = chatRoomObj.chats;
for (var message in messages) {
if (messages[message].isSystem) {
chatsArr.push(
<SystemMessage
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
} else {
chatsArr.push(
<Chat
chatObj={messages[message]}
user={user}
path={"/dms/" + chatRoomObj.room}
key={messages[message].timestamp}
/>
);
}
}
}
var chats = chatsArr.reverse();
setChats(chatsArr.reverse())
}, [chatRoomObj])
/**
* Send Message in Chatroom
@@ -48,21 +85,39 @@ export function DMRoom({ roomObj, user }) {
* @returns {void}
*/
function sendMessage(data) {
// Other UID
var otherUID = chatRoomObj.initUID == user.uid ? chatRoomObj.targetUID : chatRoomObj.initUID;
// Send other user notification if not in room
if (chatRoomObj.users && chatRoomObj.users.online) {
if (!(otherUID in chatRoomObj.users.online)) {
createNotification(
"New Message",
`${user.username} sent you a message.`,
"dm",
user.uid,
otherUID
);
}
}
var messageFilterBypass = [undefined, null, "", " ", ' ', '\'']
reset();
var payload = {
body: data.message,
user: user.username,
uid: user.uid,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/dms/${roomObj.room}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
if (!messageFilterBypass.includes(data.message)) {
var payload = {
body: data.message,
user: user.username,
uid: user.uid,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/dms/${chatRoomObj.room}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
}
}
if (!chats) return <div>No Chats</div>;
+13 -1
View File
@@ -3,7 +3,7 @@ import Link from "next/link"
// Firebase Imports
import { database } from "../../../firebase-config";
import { ref, set, remove } from "firebase/database";
import { ref, set, remove, onDisconnect, serverTimestamp } from "firebase/database";
// Component Imports
import { NotificationPanel } from "./notifications/notifications";
@@ -23,6 +23,7 @@ import CloseIcon from '@mui/icons-material/Close';
*/
function closeChat(chatRoomObj, user) {
remove(ref(database, `/rooms/${chatRoomObj.path}/${chatRoomObj.name}-${chatRoomObj.timestamp}/users/online/${user.uid}`))
}
/**
@@ -88,6 +89,17 @@ export function Header({mainTab,chatRoomObj,user,sidebarControl}) {
}
}
// Sets User Online / Offline
// Stored in header for easy code maintenance and retains user online/offline throughout app
// Makes user online
if (user.invisibleStatus == false) {
set(ref(database, `/users/${user.uid}/lastOnline`), true)
}
// Makes user offline (with last time online)
onDisconnect(ref(database, `/users/${user.uid}/lastOnline`)).set(serverTimestamp())
return (
<div className="flex m-2 rounded-lg h-[63px] bg-white shadow-2xl p-1">
<div className="flex shrink h-[60px]">
+99 -47
View File
@@ -1,51 +1,47 @@
import { Map, Marker, ZoomControl } from "pigeon-maps";
import { Map, Marker, ZoomControl, Overlay } from "pigeon-maps";
import { database } from "../../../../firebase-config";
import { ref, get } from "firebase/database";
import { useState, useEffect } from "react";
import { ref, get} from "firebase/database";
import { useState } from "react";
import ChatBubbleTwoToneIcon from '@mui/icons-material/ChatBubbleTwoTone';
import PersonOutlineTwoToneIcon from '@mui/icons-material/PersonOutlineTwoTone';
import { red } from '@mui/material/colors';
// ONLY nearby markers
function NearbyRoomMarkers({ loc, user }) {
const [markerArr, setMarkerArr] = useState([]);
/**
* Nearby Markers Grabber
* @param {JSON} location - Location Object {latitude, longitude}
* @returns {Array} - Array of Markers {<Marker>}
*/
function NearbyMarkers(location) {
const [newMarkers, setNewMarkers] = useState(null);
if (location) {
const path = String(location.latitude.toFixed(2)).replace(".", "") +"/" +String(location.longitude.toFixed(2)).replace(".", "") +"/";
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
if (snapshot.exists()) {
const rooms = snapshot.val();
setNewMarkers(rooms)
}
})
}
return newMarkers;
}
// Room path in DB
const path =
String(loc.latitude.toFixed(2)).replace(".", "") +
"/" +
String(loc.longitude.toFixed(2)).replace(".", "") +
"/";
// Sorry for the href but <Link> doesn't work here
const handleRoomMarkerClick = (roomObj) => {
window.location.href =
"/chat?room=" + path + roomObj.name + "-" + roomObj.timestamp;
};
// Mostly copied Nick's code from before
useEffect(() => {
if (loc && user) {
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
if (snapshot.exists()) {
const rooms = snapshot.val();
const newMarkers = Object.values(rooms).map((roomObj, index) => {
const markerKey = roomObj.path + "-" + index;
return (
// Want to change this to be something other than markers (or something extra)
<Marker
key={markerKey}
anchor={[roomObj.latitude, roomObj.longitude]}
color="blue"
onClick={() => handleRoomMarkerClick(roomObj)}
></Marker>
);
});
setMarkerArr(newMarkers);
/**
* Friend Markers Grabber
* @param {JSON} user - User Object
* @returns {Array} - Array of Markers {<Marker>}
*/
function FriendMarkers(user) {
var friendMarkers = []
if (user && "friends" in user && "friends" in user.friends) {
for (var friend in user.friends.friends) {
get(ref(database, `/users/${friend}`)).then((snapshot) => {
var friendData = snapshot.val();
if (friendData.location) {
friendMarkers.push(friendData);
}
});
}
}, []);
return markerArr;
}
}
/**
@@ -57,11 +53,49 @@ function NearbyRoomMarkers({ loc, user }) {
* @prop {Boolean} markers - Enable Markers
* @returns {Map} - Geo Component (As Map)
*/
export function Geo({ loc, zoom, moveable, markers, user }) {
const handleUserMarkerClick = () => {
window.location.href = "/user?uid=" + user.uid;
export function Geo({ loc, zoom, moveable, user }) {
const [hovering, setHovering] = useState(false);
const [hoverText, setHoverText] = useState("");
const [hoverAnchor, setHoverAnchor] = useState([null,null]);
if (moveable) {
if (user.rooms) {
// Load My Rooms Markers
var myRoomsMarkers = Object.values(user.rooms).map((roomObj) => {
return (<Marker
key={roomObj.path + "-" + roomObj.name}
anchor={[roomObj.latitude, roomObj.longitude]}
onClick={() => {window.location.href = "/chat?room=" + roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp;}}
style={{pointerEvents:'auto'} /* So stupid */}
onMouseOver={() => {setHoverText(roomObj.name);setHovering(true);setHoverAnchor([roomObj.latitude, roomObj.longitude])}}
onMouseOut={() => {setHovering(false)}}
>
<ChatBubbleTwoToneIcon color="primary" fontSize="large"/>
</Marker>)
})
}
// Load Nearby Markers
var nearbyMarkers = NearbyMarkers(loc);
if (nearbyMarkers) {
var nearbyMarkers = Object.values(nearbyMarkers).map((roomObj) => {
return (<Marker
key={roomObj.path + "-" + roomObj.name}
anchor={[roomObj.latitude, roomObj.longitude]}
onClick={() => {window.location.href = "/chat?room=" + roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp;}}
style={{pointerEvents:'auto'} /* So stupid */}
onMouseOver={() => {setHoverText(roomObj.name);setHovering(true);setHoverAnchor([roomObj.latitude, roomObj.longitude])}}
onMouseOut={() => {setHovering(false)}}
>
<ChatBubbleTwoToneIcon color="secondary" fontSize="large"/>
</Marker>)
})
}
}
if (!loc) {
return <div>Getting Location...</div>;
} else {
@@ -72,11 +106,29 @@ export function Geo({ loc, zoom, moveable, markers, user }) {
defaultZoom={zoom}
mouseEvents={moveable}
touchEvents={moveable}
attribution={false}
>
{zoom && <ZoomControl />}
{markers && NearbyRoomMarkers({ loc, user })}
{moveable && nearbyMarkers}
{moveable && myRoomsMarkers}
{ /* Overlay */}
{hovering && (
<Overlay anchor={hoverAnchor} offset={[0, 0]}>
<div className="bg-white rounded-lg p-2">
<p className="text-lg">{hoverText}</p>
</div>
</Overlay>
)}
{user && ( // Shows the user marker
<Marker anchor={[loc.latitude, loc.longitude]} color="red" onClick={handleUserMarkerClick} />
<Marker
anchor={[loc.latitude, loc.longitude]}
color="red"
style={{pointerEvents:'auto'} /* So stupid */}
>
<PersonOutlineTwoToneIcon sx={{ color: red[500] }} fontSize="large"/>
</Marker>
)}
</Map>
</>
@@ -17,28 +17,38 @@ import { ref, set, remove } from "firebase/database";
* @returns {Notification} - Notification Component
*/
function Notification({data}) {
/**
* Removes Notification
* @returns {void}
*/
function removeNotification() {
remove(ref(database, `/users/${data.ruser}/notifications/${data.suser}-${data.action}`))
}
/**
* Determines Action
*/
function onClick() {
if (data.action === "dm") {
var order = data.suser > data.ruser ? data.ruser + "-" + data.suser : data.suser + "-" + data.ruser;
window.location.href = "/dm?dm=" + order;
removeNotification()
}
}
return (
<div className="hover:bg-[#C0C0C0] rounded-lg">
<div className="hover:bg-[#C0C0C0] rounded-lg cursor-pointer" >
<div className="float-right top-0 cursor-pointer p-2 text-[24px] text-slate-500">
<div onClick={() => {removeNotification(data.id)}}><CloseIcon/></div>
<div onClick={() => {removeNotification()}}><CloseIcon/></div>
</div>
<div className="p-3 text-left">
{data.title}<br/>
<div className="p-3 text-left" onClick={() => {onClick()}}>
<span className="font-bold">{data.title}</span><br/>
{data.byline}<br/>
</div>
</div>
)
}
/**
* Removes Notification
* @param {String} ruser - Receiving user UID (User Whose Notifications are being removed)
* @param {String} dataID - Notification ID
* @returns {void}
*/
function removeNotification(ruser, dataID) {
remove(ref(database, `/user/${ruser}/notifications/${dataID}`))
}
/**
* Creates New Notification
@@ -49,16 +59,18 @@ function removeNotification(ruser, dataID) {
* @param {String} ruser - Receiving user UID
* @returns {void}
*/
function createNotification(title, byline, action, suser, ruser) {
export function createNotification(title, byline, action, suser, ruser) {
var timestamp = new Date().getTime();
var payload = {
title: title,
byline: byline,
action: action,
suser: suser,
ruser: ruser
ruser: ruser,
id: suser + "-" + action,
timestamp: timestamp
};
set(ref(database, `/user/${ruser}/notifications/${timestamp}-${suser}`), payload);
set(ref(database, `/users/${ruser}/notifications/${suser}-${action}`), payload);
}
/**
@@ -69,14 +81,13 @@ function createNotification(title, byline, action, suser, ruser) {
*/
export function NotificationPanel({user}) {
var notificationsMap = []
if (user.notification) {
for (var notificationPackage in user.notification) {
notificationsMap.push(<Notification data={user.notification[notificationPackage]}/>)
if (user.notifications) {
for (var notificationPackage in user.notifications) {
notificationsMap.push(<Notification data={user.notifications[notificationPackage]}/>)
}
} else {
notificationsMap = null
}
var isNotifications = true
} else {
var isNotifications = false
}
return (
<Popover className="relative">
@@ -88,8 +99,8 @@ export function NotificationPanel({user}) {
<Popover.Panel className="absolute z-10 bg-white mt-[4px] rounded-xl ml-3 shadow-2xl w-64 md:right-[0px] max-md:right-[-300%]">
<div className="grid grid-cols-1">
{isNotifications && notificationsMap}
{!isNotifications &&
{notificationsMap}
{!notificationsMap &&
<div className="h-[64px] flex flex-col justify-center items-center">
<NotificationsPausedIcon/> All caught up!
</div>
+71 -39
View File
@@ -2,7 +2,7 @@
import { Form, useForm } from "react-hook-form";
// Firebase Imports
import { ref, set } from "firebase/database";
import { ref, set, onChildAdded, onChildRemoved } from "firebase/database";
import { database } from "../../../../firebase-config";
// Component Imports
@@ -10,6 +10,7 @@ import { Chat, SystemMessage } from "../datatypes";
// Icons
import SendIcon from '@mui/icons-material/Send';
import { useState,useEffect } from "react";
/**
* Chat Room Component
@@ -18,53 +19,84 @@ import SendIcon from '@mui/icons-material/Send';
* @returns {Object} - Chat Room Component
*/
export function ChatRoom({ roomObj, user }) {
const [chatRoomObj, setChatRoomObj] = useState(roomObj);
const [chats, setChats] = useState(null);
var { register, control, reset, handleSubmit } = useForm();
// Listeners for Chats
useEffect(() => {
var path = chatRoomObj.path + "/" + chatRoomObj.name + "-" + chatRoomObj.timestamp;
onChildAdded(ref(database, `/rooms/${path}/chats`), (newChat) => {
var newChatRoomObj = chatRoomObj
if (newChatRoomObj) {
if (!newChatRoomObj.chats) {
newChatRoomObj.chats = {}
}
newChatRoomObj.chats[newChat.key] = newChat.val()
setChatRoomObj({...newChatRoomObj})
}
// Message updater
var chatsArr = [];
var messages = roomObj.chats;
for (var message in messages) {
if (messages[message].isSystem) {
chatsArr.push(
<SystemMessage
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
} else {
chatsArr.push(
<Chat
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
});
onChildRemoved(ref(database, `/rooms/${path}/chats`), (removed) => {
if (chatRoomObj) {
var newChatRoomObj = chatRoomObj
var deleted = removed.val()
delete newChatRoomObj.chats[`${deleted.timestamp}-${deleted.user}`]
setChatRoomObj({...newChatRoomObj})
}
});
}, [])
useEffect(() => {
// Message updater
var chatsArr = [];
var messages = chatRoomObj.chats;
for (var message in messages) {
if (messages[message].isSystem) {
chatsArr.push(
<SystemMessage
chatObj={messages[message]}
key={messages[message].timestamp}
/>
);
} else {
chatsArr.push(
<Chat
chatObj={messages[message]}
user={user}
path={"/rooms/" + chatRoomObj.path + "/" + chatRoomObj.name + "-" + chatRoomObj.timestamp}
key={messages[message].timestamp}
/>
);
}
}
}
var chats = chatsArr.reverse();
setChats(chatsArr.reverse())
}, [chatRoomObj])
/**
* Send Message in Chatroom
* @param {JSON} data - Message data to send (from form)
* @returns {void}
*/
function sendMessage(data) {
reset();
var payload = {
body: data.message,
user: user.username,
uid: user.uid,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/rooms/${
roomObj.path + "/" + roomObj.name + "-" + roomObj.timestamp
}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
var messageFilterBypass = [undefined, null, '', ' ', '\'', '\"']
if (!messageFilterBypass.includes(data.message)) {
reset();
var payload = {
body: data.message,
user: user.username,
uid: user.uid,
isSystem: false,
timestamp: new Date().getTime(),
};
set(
ref(
database,
`/rooms/${chatRoomObj.path + "/" + chatRoomObj.name + "-" + chatRoomObj.timestamp}/chats/${new Date().getTime()}-${user.username}`
),
payload
);
}
}
if (!chats) return <div>No Chats</div>;
@@ -45,12 +45,11 @@ export function ProfileEdit({ profileData, user, onSave }) {
}
);
} else {
for (var key in data) {
if (data[key] == "") {
for (var key in profileData) {
if (data[key] == "" || (key == "pfp" && data[key].length == 0)) {
data[key] = profileData[key];
}
}
data.pfp = profileData.pfp;
handleEditState(false);
update(ref(database, `users/${user.uid}`), data);
}
@@ -63,7 +62,7 @@ export function ProfileEdit({ profileData, user, onSave }) {
<div>
<img
src={profileData.pfp}
width="150px"
style={{width: "150px", maxHeight: "400px"}}
className="relative mx-auto rounded-2xl overflow-hidden"
/>
Current Profile Picture
@@ -3,8 +3,9 @@ import { Popover } from "@headlessui/react";
import Link from "next/link"
// Firebase Imports
import { auth } from "../../../../firebase-config";
import { auth, database } from "../../../../firebase-config";
import { signOut } from "firebase/auth";
import {update, ref, serverTimestamp} from "firebase/database";
/**
* Logs out from Firebase Authentication
@@ -25,10 +26,10 @@ export function ProfilePanel({user}) {
<Popover.Button as="div">
<div className="h-[44px] p-[2px] pr-[15px] cursor-pointer bg-cyan-500 text-white font-bold rounded-full shadow-2xl flex">
<div className="flex items-center pl-1">{user.firstName}</div>
<div className="ml-3 rounded-lg">
<div className="ml-3 rounded-lg flex items-center">
<img
src={user.pfp}
width="40px"
style={{maxWidth: "40px", maxHeight: "40px"}}
className="relative mx-auto rounded-xl overflow-hidden"
/>
</div>
@@ -43,6 +44,18 @@ export function ProfilePanel({user}) {
>
View Profile
</Link>
<div className="rounded-xl p-4 hover:bg-[#C0C0C0] cursor-pointer"
onClick={() => {
// Toggle Invisible Status
update(ref(database, `/users/${user.uid}`), {
invisibleStatus: user.invisibleStatus? !user.invisibleStatus: true,
lastOnline: user.invisibleStatus? true: serverTimestamp()
}
);
}}
>
{user.invisibleStatus ? "Go Online" : "Go Invisible"}
</div>
<Link
className="rounded-xl p-4 hover:bg-[#C0C0C0]"
onClick={logout}
+13 -13
View File
@@ -1,23 +1,23 @@
import { Member } from "../datatypes"
import { database } from "../../../../firebase-config"
import {ref, get, set} from "firebase/database"
import {ref, get, onValue} from "firebase/database"
import { useState, useEffect } from "react"
export function Sidebar({user, chatRoomObj}) {
const [profileData, setProfileData] = useState(null)
// Active users list
if (
chatRoomObj.hasOwnProperty("users") &&
chatRoomObj.users.hasOwnProperty("online")
) {
var activeUsers = [];
var activeUsersJSON = chatRoomObj.users.online;
const [profileData, setProfileData] = useState(null)
const [chatroomOnline, setChatroomOnline] = useState(null)
var path = chatRoomObj.UIDs[0] < chatRoomObj.UIDs[1] ? chatRoomObj.UIDs[0] + "-" + chatRoomObj.UIDs[1] : chatRoomObj.UIDs[1] + "-" + chatRoomObj.UIDs[0];
var activeUsers = []
onValue(ref(database, `/dms/${path}/users/online`), (snapshot) => {
if (snapshot.exists()) {
var activeUsersJSON = snapshot.val();
for (var activeUser in activeUsersJSON)
activeUsers.push(<Member memberObj={activeUsersJSON[activeUser]} />);
var chatroomOnline = activeUsers
activeUsers.push(<Member memberObj={activeUsersJSON[activeUser]} key={activeUser} />);
}
})
useEffect(() => {
if (user) {
@@ -42,14 +42,14 @@ export function Sidebar({user, chatRoomObj}) {
<div className="m-2 h-[98%] grid grid-cols-1">
<div className="flex place-content-center">
<div className="bg-white rounded-lg m-2 shadow-2xl pt-10">
<img src={profileData.pfp} className="w-[80%] relative mx-auto"/>
<img src={profileData.pfp} style={{maxHeight: "800px"}} className="w-[80%] relative mx-auto"/>
<div className="font-bold text-[24px]">{profileData.firstName} {profileData.lastName}</div>
@{profileData.username}
</div>
</div>
<div className="bg-white rounded-lg m-2 shadow-2xl">
<div>In The Chat</div>
{chatroomOnline}
{activeUsers}
</div>
</div>
</div>
@@ -10,6 +10,7 @@ import { database } from "../../../../firebase-config";
import { ref, set, get } from "firebase/database";
// Component Imports
import { NearbySidebar } from "./nearby";
import { ChatRoomSidebar } from "../datatypes";
// Friend Imports (TEMP)
@@ -88,50 +89,27 @@ function classNames(...classes) {
* @returns {Object} - App Page Sidebar Component
*/
export function Sidebar({user,location,loadingLoc}) {
const [nearbyArr, setNearbyArr] = useState([])
const [nearbyArrReady, setNearbyArrReady] = useState(false)
const [friends, setFriends] = useState([])
const [friendRequests, setFriendRequests] = useState(null)
const [dms, setDMs] = useState(null)
// Add myRooms to Sidebar
var myRoomArr = [];
for (var room in user.rooms) {
get(ref(database, `/rooms/${user.rooms[room].path}/${user.rooms[room].name}-${user.rooms[room].timestamp}`)).then((snapshot) => {
var newRoom = (
<ChatRoomSidebar
roomObj={snapshot.val()}
key={snapshot.val().timestamp}
/>
);
myRoomArr.push(newRoom);
})
}
const [dms, setDMs] = useState((<div>No DMs</div>))
const [myRoomArr, setMyRoomArr] = useState([])
useEffect(() => {
var nearbyArr = []
if (location) {
var path = String(location.latitude.toFixed(2)).replace(".", "") + "/" + String(location.longitude.toFixed(2)).replace(".", "");
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
// Add nearby to Sidebar
if (snapshot.exists()) {
var rooms = snapshot.val()
for (var room in rooms) {
var newRoom = (
<ChatRoomSidebar
roomObj={rooms[room]}
key={rooms[room].timestamp}
/>
);
nearbyArr.push(newRoom);
}
} else {
nearbyArr.push(<div className="pt-5">No Nearby Rooms<br />Create One?</div>)
}
setNearbyArr(nearbyArr)
setNearbyArrReady(true)
var myRoomArr = [];
// Add myRooms to Sidebar
for (var room in user.rooms) {
get(ref(database, `/rooms/${user.rooms[room].path}/${user.rooms[room].name}-${user.rooms[room].timestamp}`)).then((snapshot) => {
var newRoom = (
<ChatRoomSidebar
roomObj={snapshot.val()}
key={snapshot.val().timestamp}
/>
);
myRoomArr.push(newRoom);
})
}
}, [location])
setMyRoomArr(myRoomArr)
}, [])
useEffect(() => {
if (user && user.friends) {
@@ -164,18 +142,17 @@ export function Sidebar({user,location,loadingLoc}) {
get(ref(database, `/users/${dmsList[dmRoom].UIDs[1]}`)).then((snapshot) => {
var friendObj = snapshot.val()
dmArr.push(<DM user={user} friendObj={friendObj} key={dmRoom}/>);
setDMs(dmArr);
})
} else if (user.uid == dmsList[dmRoom].UIDs[1]) {
get(ref(database, `/users/${dmsList[dmRoom].UIDs[0]}`)).then((snapshot) => {
var friendObj = snapshot.val()
dmArr.push(<DM user={user} friendObj={friendObj} key={dmRoom}/>);
setDMs(dmArr);
})
}
}
if (dmArr.length == 0) {
dmArr.push(<div>No DMs</div>);
}
setDMs(dmArr);
})
}, [user])
@@ -190,7 +167,7 @@ export function Sidebar({user,location,loadingLoc}) {
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>Nearby</Tab>
)}>Nearby</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
@@ -211,29 +188,28 @@ export function Sidebar({user,location,loadingLoc}) {
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>DMs</Tab>
)}>DMs</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>Friends</Tab>
)}>Friends</Tab>
<Tab className={({ selected }) =>
classNames(
'w-[30%]',
selected
? 'bg-cyan-500 text-white font-bold shadow hover:bg-white/[0.6] hover:text-black'
: 'hover:bg-cyan-500/[0.6] hover:text-white hover:font-bold'
)} defaultIndex={1}>Requests</Tab>
)}>Requests</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<div className="overflow-y-auto h-[90%]">
<div>
{loadingLoc && <div>Loading...</div>}
{nearbyArrReady && nearbyArr}
<NearbySidebar location={location}/>
</div>
</div>
</Tab.Panel>
@@ -0,0 +1,84 @@
import {useEffect, useState} from "react";
import {database} from "../../../../firebase-config";
import {get, ref} from "firebase/database";
import { useForm } from "react-hook-form"
import { ChatRoomSidebar } from "../datatypes";
export function NearbySidebar({location}) {
const [nearbyArr, setNearbyArr] = useState([])
const [displayedRooms, setDisplayedRooms] = useState([])
const [nearbyArrReady, setNearbyArrReady] = useState(false)
const {register, watch, setFocus} = useForm({defaultValues: {search: null}})
// Search Bar Value
const search = watch("search")
// Search Bar Component
function SearchBar() {
return (
<div className="w-[97%]">
<input type="text" placeholder="Search" {...register("search")} className="w-full p-2 border-2 border-gray-300 rounded-lg col-span-3" value={null} />
</div>
)
}
// Filters Rooms Based on Search
useEffect(() => {
if (search != "") {
var rooms = []
for (var nearbyRoom in nearbyArr) {
if (nearbyArr[nearbyRoom].props.roomObj.name.toLowerCase().includes(search.toLowerCase()) || nearbyArr[nearbyRoom].props.roomObj.description.toLowerCase().includes(search.toLowerCase())) {
rooms.push(<ChatRoomSidebar roomObj={nearbyArr[nearbyRoom].props.roomObj} key={nearbyArr[nearbyRoom].props.roomObj.timestamp}/>)
}
}
} else {
rooms = nearbyArr
}
setDisplayedRooms(rooms)
}, [search])
// Returns cursor to search bar on render
useEffect(() => {
setFocus("search")
}, [displayedRooms])
// Sets Initial Array of Nearby Rooms
useEffect(() => {
var nearbyArr = []
if (location) {
var path = String(location.latitude.toFixed(2)).replace(".", "") + "/" + String(location.longitude.toFixed(2)).replace(".", "");
get(ref(database, `/rooms/${path}`)).then((snapshot) => {
// Add nearby to Sidebar
if (snapshot.exists()) {
var rooms = snapshot.val()
for (var room in rooms) {
var newRoom = (
<ChatRoomSidebar
roomObj={rooms[room]}
key={rooms[room].timestamp}
/>
);
nearbyArr.push(newRoom);
}
} else {
nearbyArr.push()
}
setNearbyArr(nearbyArr)
setDisplayedRooms(nearbyArr)
setNearbyArrReady(true)
})
}
}, [location])
return (
<div>
<SearchBar/>
{nearbyArrReady && displayedRooms}
{!nearbyArrReady && <div>Loading...</div>}
{nearbyArrReady && nearbyArr.length === 0 && <div className="pt-5">No Nearby Rooms<br />Create One?</div>}
</div>
)
}