diff --git a/package-lock.json b/package-lock.json
index 253aecd..2db436b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@googlemaps/js-api-loader": "^2.1.0",
+ "@mediapipe/tasks-vision": "^0.10.35",
"firebase": "^12.14.0",
"ngeohash": "^0.6.3"
},
@@ -755,6 +756,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@mediapipe/tasks-vision": {
+ "version": "0.10.35",
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.35.tgz",
+ "integrity": "sha512-HOvadwVRE6JC+45nyYhmnywnr5h/J8KZvOeUNVOG9q/0875pZgItznFB9bRTvLc264YSJqiZ1NsIpCStJw/egg==",
+ "license": "Apache-2.0"
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
diff --git a/package.json b/package.json
index 2adeda2..9e83d5f 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
},
"dependencies": {
"@googlemaps/js-api-loader": "^2.1.0",
+ "@mediapipe/tasks-vision": "^0.10.35",
"firebase": "^12.14.0",
"ngeohash": "^0.6.3"
}
diff --git a/src/lib/Firebase/storage.js b/src/lib/Firebase/storage.js
new file mode 100644
index 0000000..47d2a48
--- /dev/null
+++ b/src/lib/Firebase/storage.js
@@ -0,0 +1,10 @@
+import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'
+import { storage } from './config.js';
+
+export async function uploadImage(file) {
+ //create unique file name and stores them in one folder in firebase
+ const filename = `messages/${Date.now()}_${file.name}`;
+ const storageRef = ref(storage, filename);
+ await uploadBytes(storageRef, file);
+ return getDownloadURL(storageRef);
+}
\ No newline at end of file
diff --git a/src/lib/components/BottomSheet.svelte b/src/lib/components/BottomSheet.svelte
index 9b4e001..3a2de26 100644
--- a/src/lib/components/BottomSheet.svelte
+++ b/src/lib/components/BottomSheet.svelte
@@ -22,6 +22,9 @@
{#if message}
+ {#if message.imageUrl}
+

+ {/if}
{message.text}
{#if decay}
left {decay.daysAgo} days ago. fading in {decay.daysLeft} days.
@@ -114,6 +117,14 @@
animation: pulsate 1.5s ease-in-out 3;
}
+ .message-img{
+ width: 100%;
+ max-height: 220px;
+ object-fit: cover;
+ border-radius: 12px;
+ margin-bottom: 0.75rem;
+ }
+
@keyframes pulsate {
0%, 100% {box-shadow: 0 0 0 0 rgba(78,205,196,0.4); }
50% {box-shadow: 0 0 0 12px rgba(78, 205, 196, 0);}
diff --git a/src/lib/components/ComposeSheet.svelte b/src/lib/components/ComposeSheet.svelte
index 50c662b..f43a644 100644
--- a/src/lib/components/ComposeSheet.svelte
+++ b/src/lib/components/ComposeSheet.svelte
@@ -2,6 +2,9 @@
import { mapStore } from '$lib/stores/mapStore.js';
import { messagesStore } from '$lib/stores/messagesStore.js';
import { addMessage } from '$lib/firebase/messages.js'
+ import { hasFace } from '$lib/utils/faceDetection.js';
+ import { uploadImage } from '$lib/Firebase/storage.js';
+
let {lat, lng} = $props();
@@ -9,23 +12,78 @@
let submitting = $state(false);
let remaining = $derived(240-text.length);
+ let selectedFile = $state(null);
+ let imagePreview = $state(null); // what
![]()
will read
+ let imageError = $state(null);
+ let checkingFace = $state(false); // this will show the checking image... message
+
+ async function handleImageSelect(event) {
+ const file = event.target.files[0]; // only one file will be allowed so always index 0
+
+ if (!file) return; // no file do nothing
+ checkingFace = true; // show loading state
+ imageError = null; // clear errors
+
+ try {
+ // run face detection
+ const faceFound = await hasFace(file);
+
+ if (faceFound) {
+ // no upload & error
+ imageError = 'Images may not contain faces. Please choose another photo.';
+
+ //reset file input for other attempt
+ event.target.value = '';
+ selectedFile= null;
+ imagePreview = null;
+ } else {
+ // no face we move on
+ selectedFile = file;
+ // create preview
+ imagePreview = URL.createObjectURL(file);
+ }
+
+ } catch (err) {
+ // fixed: previously allowed the upload on any detection error — wrong for a
+ // face-blocking feature. Now we surface the error and block the upload instead.
+ console.error('[faceDetection] error:', err);
+ imageError = 'Could not verify the image. Please try a different photo.';
+ event.target.value = '';
+ selectedFile = null;
+ imagePreview = null;
+ } finally {
+ // no matter what turn the loading state off
+ checkingFace = false;
+ }
+ }
+
async function handleSubmit() {
+ // no submit with no text or text over limit
if (!text.trim() || remaining < 0) return;
+
submitting = true;
+ let imageUrl = ''; // stays empty if no image
- await addMessage(lat, lng, text.trim());
+ if (selectedFile) {
+ imageUrl = await uploadImage(selectedFile);
+ }
- //refresh the messages store so pin is there
- const { getNearbyMessages } = await import('$lib/firebase/messages.js');
+ await addMessage(lat, lng, text.trim(), imageUrl);
+
+ // reload pins so that they show up with new message
+ const { getNearbyMessages } = await import ('$lib/firebase/messages.js');
const updated = await getNearbyMessages(lat, lng);
messagesStore.set(updated);
- //reset and close
+ // reset compose state
text = '';
+ selectedFile = null;
+ imagePreview = null;
submitting = false;
- mapStore.set({selectedMessage: null, composing: false});
- }
+ // close the sheet
+ mapStore.set({selectedMessage: null, composing:false});
+ }
@@ -45,6 +103,35 @@
rows="5"
>
+
+ {#if checkingFace}
+
Checking image...
+
+ {:else if imagePreview}
+
+

+
+
+
+
+ {:else}
+ {#if imageError}
+
{imageError}
+ {/if}
+
+ {/if}
+
+