**Name:** Samantha Lopez 

**ID:** 20266142

**Email:** samantha@kaist.ac.kr

**Gittea Repo:** [https://git.prototyping.id/20266142/Overheard.git](https://git.prototyping.id/20266142/Overheard.git)

**Video Demo:** [https://youtu.be/pI6eU5PSqRY](https://youtu.be/pI6eU5PSqRY)

**Live site URL** [https://overheard-d7396.web.app](https://overheard-d7396.web.app)

# App Description

Overheard is a location based message board. Its premise is simple and intentionally constrained, you are only able to read what others have left at a place if you are physically in the same location. You can only leave a message if you are standing there too. Overheard functions on anonymity It strays away from becoming a "social media" by eliminating profiles, feeds, and online identities. The app utilizes firebase's anonymous authenticatioin to store activity on devices, enough to link interactions across sessions but not enough to create anythings that ties your messages back to you. 

Overheard works like this; upon opening, the app gets your location and translates coordinates into a geohash, a short string representative of your geogrpahical location. It then takes that string to look for messages whose geohash contain an identical prefix thus returning messages left around you. The messages appear as pins on the map. You can tap on any pin to read what was left there. When reding a message you are presented with two choices: to echo it which restarts its 30 day life span and thus extends its visibility on the map, or let it go, which simply closes the read view of the messages. You can also leave your own messages compromised of text and optionally an image for others to read within the next 30 days, you can even choose a display pin color for your message(optional). 

## Echoing messages

one of my favorite features of the entire app is the 30 day decay feature that each message lives under. This makes it so messages don't accumulate forever, they face, a message which goes unechoed dissapears within 30 days, not because it gets deleted from the database but because it was either undiscovered or because no one made the decision to extend its lifespan. Echoing a message resets its lifespan it signifies that someone cared enough to keep it alive. This interaction allows messages to become curated over time, messages that survive are those who kept being encountered by people who wanted those messages to survive. This si what makes the app interesting. Individual messages are fleeting, echoing is what makes something as close to permanent as you can get within this app and physical presence is required for all of these interactions.  

## Features

**Geoloacation based message visiibility:** Overheard uses the browser Geolocation API to get user coordinates then turns them into a geohash (using `ngeohash`). The geohash is then used to look for messages in firebase within the desired area (location scope is calculated based on geohash prefix similarity, currently the prefix is 7 characters which is about a city blocks worth of distance). This feature makes physical presence a requirement to interact with the app in any meaningful way. 

**Message pins on the map:** Each nearby message is visible as a pastel colored circle on the map. This is done via Google Maps legacy Marker API with the icon being handled by `pins.js`. Pin color can be chosen at the time of leaving a message otherwise a randomly generated color will  be used. Opted for this view over a feed to reinforce spacial memory, messages live in the locations where they were left not in a social media like feed. 

**30-day Decay:** Messages had a `lastEchoAt` timestamp. Messages whose last echo was more than 30 days ago get filtered out and not rendered thus making them dissapear entirely from user view. 

**Echo:** Increments the messages `echoCount` and sets the messages `lastEchoAt` to the current timestamp, reseting their 30 day decay clock. Plays a small arpeggio like sound as feeback when pressed.

**Let go:** Pressing this simpley closes the detailed message view. The alternative to echoing somethings should be equally as effortless and framed as letting a memory go. 

**Composing a message:** `ComposeSheet.svelte` calls `addMessage` with the text, image, and pin color. Maximum 240 characters are accepted. Images are analyzed to ensure that no selfies are uploaded to preserve anonymity.

**Share with link and QR code:** `SharePopover.svelte` creates a URL and a QR code (via a qrcode package). `+page.svelte` is able to read the the `?message` parameter on the url once it opens and shows that message (if it is still alive). I decided to add this feature as it brings some of the sharing and allows for even more physical interactions. For instance the QR code can be posted in places both sharing a message and inviting people to share their own.

**location trail:** Every time the app loads, `addTrailPoint` writes the coordinates to localStorage. In `MapView.svelte`, the full history is drawn as a polyline on the map (toggleable with the small button in the top-left). A visualizer of all the places where you have shared and received memories within Overheard.  

**Archive page:** Shows a list of every message that has been left on the device and shows their decay status.`getMyMessages()` looks for messages in Firestore by authorId matching the anonymous UID. This allows you to be able to see what you've left behind and whether it's still alive. 

**Stamps:** Every time a message is posted from a new geohash-4 region (4 char prefix location precision), `checkAndAwardStamp` reverse-geocodes the coordinates and writes a stamp to `users/{uid}/stamps/{geohash4}`. The stamp page shows all the earned stamps like a sticker-album layout. Each stamp is a circle with a predetermined icon and the place name. This serves as a track of all the places  you've been in and in which you choose to partake in the sharing of anonymous memories.

**Face detection for photo uploads:** When you select a photo `hadFace(file)` from `faceDetection.js` block the upload of a picture if any face is detected. The check occurs before the image even gets to be previwed for upload. Since overheard is anonymous its pictures should also be.

## App summary; TLDR

Overheard was not built ot be a social app, it purposely erased the ability to create a public identity, you cannot follow anyone, see anyones profile, or even know who left a message in the first place. Its has no guarantee of permanence, participating is accepting that the memories you share and receive might fade. It reinforces physical presence, you cannot sit and scroll through messages but you can only experience the memories of others by crating some yourself in the same physical location. Overheard is designed to feel like you are reading something somene left for you in a particular place, not scrolling for content. 

# Code Organization

## File Organization
Overheard is an application built using SvelteKit and which was deployed to firebase for hosting. The file structure it functions under is as illustrated below. 

![File organization diagram](src/lib/assets/File_organization_overheard.png)

## Data/Application Flows

**App opens**

![App opens diagram](src/lib/assets/open%20app.png)

**Message pins render**

![Message pins render diagram](src/lib/assets/Message%20pins%20load.png)

**Tapping a pin**

![Tapping a pin diagram](src/lib/assets/Tapping%20a%20pin.png)

**Leaving a message**

![Leaving a message diagram](src/lib/assets/posting%20message.png)

**New message is added during active session**

![New Message added during active session diagram](src/lib/assets/New%20pin%20dropped.png)

**View the diagrams on Figma with the following link:**
[https://www.figma.com/board/zJCrMvicQFHpyX9eqeCY2W/Final-Project-Diagrams?node-id=2008-3984&t=NF572Ti6bMLwkJB1-1](https://www.figma.com/board/zJCrMvicQFHpyX9eqeCY2W/Final-Project-Diagrams?node-id=2008-3984&t=NF572Ti6bMLwkJB1-1)

## Svelte stores (shared states)

**messagesStore.js**
|Variable/Function|Purpose|
|-----------------|-------|
|`messageStore`|array of all the nearby active messages|
|`livePinIdsStore`| set of message ID's that arrive bia the real time listener|
|`setMessages(newMessages)`|setter that compares against previous list and plays `playNewPinchime()` if any new IDs appear|

**mapStore.js**
|Variable/Function|Purpose|
|-----------------|-------|
|`mapStore`| help figure out what to render|
|`selectedMessage`|the message tapped on by the user (null if none)|
|`composing`| `true` when the compose sheet is open|

**userStore.js**
|Variable/Function|Purpose|
|-----------------|-------|
|`userStore`|holds the anonymous Firebase identity for the device|
|`uid`|the anonymous Firebase UID (needed before stamps or archive can load)|
|`ready`|true once auth has resolved|
|`initAuth()`|calls signInAnonymously and subscribes to onAuthStateChanged to keep the store in sync|

**stampStore.js**
|Variable/Function|Purpose|
|-----------------|-------|
|`stampsStore`| array of earned passport stamps (populated on load, appended to live when a new stamp is earned)|

**globalCountStore.js**
|Variable/Function|Purpose|
|-----------------|-------|
|`globalCount`|	total messages ever posted worldwide|
|`refreshGlobalCount()`|fetches from meta/stats in Firestore and updates the store|


## How different data is stored in Firestore


**Messages**
```
authorId: "T9R85su4FvUII22QMUUljK0jyy33"        //(string)
createdAt: May 17, 2026 at 7:34:03 PM UTC+9     //(timestamp)
echoCount: 0                                    //(int64)
geohash: "wy6wfhcqd"                            //(string)
imageUrl: ""                                    //(string)
lastEchoAt: May 17, 2026 at 7:34:03 PM UTC+9    //(timestamp)
lat: 36.370018                                  //(double)
lng: 127.355324                                 //(double)
moodColor: "hsl(40, 60%, 72%)"                  //(string)
text: "cocumentation example"                   //(string)
```

**Meta Stats**
```
id: ""                                          //(string)
totalMessagesEverPosted: 0                      //(int64)
```
 
**Users**
```
uid: ""                                         //(string)
```

**Stamps**
```
geohash4: "precision-4 geohash"                 //(string)
city:  ""                                       //(string)
country: ""                                     //(string)
iconId: ""                                      //(string)
color: "hsl(...)"                               //(string)
earnedAt: May 17, 2026 at 7:34:03 PM UTC+9      //(timestamp)
```
## Whats in the .env file

| Variable | Description | Where I found |
| --- | --- | --- |
| `PUBLIC_FIREBASE_API_KEY` | Firebase Web API key | Firebase Console → Project Settings → Your apps |
| `PUBLIC_FIREBASE_AUTH_DOMAIN` | Firebase Auth domain | Same — `{projectId}.firebaseapp.com` |
| `PUBLIC_FIREBASE_PROJECT_ID` | Firebase project ID | Same |
| `PUBLIC_FIREBASE_STORAGE_BUCKET` | Firebase Storage bucket | Same — `{projectId}.appspot.com` |
| `PUBLIC_FIREBASE_MESSAGING_SENDER_ID` | Firebase messaging sender ID | Same |
| `PUBLIC_FIREBASE_APP_ID` | Firebase app ID | Same |
| `PUBLIC_MAPS_KEY` | Google Maps JavaScript API key (also used for the Geocoding REST API) | Google Cloud Console → APIs & Services → Credentials |


# Opportunities for improvement

- The current decay logic works by simply not rendering the messages which are past the 30 day threshold after their last echo. However, these messaages never get deleted from the Firestore which means they are never truly gone. 
- The map does not update as the user is moving. Currently the app only calculates messages and location on open or on refresh. Making it so the app updates location as the user is moving could add to the interacton significantly. 
- The current face detection was utilized using a model trained on close range faces and works best on selfies, thus currently some mid to far range pictures with faces can bypass this check. 

# Resource auknoledgement

Below is a comprhensive list of everything utilized to being Overheard to life (including links and usage notes) 

**Google Maps JavaScript API**
[https://developers.google.com/maps/documentation/javascript/overview](https://developers.google.com/maps/documentation/javascript/overview)

renders the interactive map, places custom pins (via `google.maps.Marker` with SVG icons), handles click interactions, and applies the custom desaturated/parchment visual styling via the `styles` array.

**Google Maps Geocoding API**

[https://developers.google.com/maps/documentation/geocoding/overview]([https://developers.google.com/maps/documentation/geocoding/overview])

Used for the stamps feature, converts a message's lat/lng coordinates into a city and country name when a user posts from a new region.

**@googlemaps/js-api-loader**

[https://www.npmjs.com/package/@googlemaps/js-api-loader]([https://www.npmjs.com/package/@googlemaps/js-api-loader])

A library used to cleanly load the Google Maps JavaScript API script asynchronously inside Svelte's `onMount`.

**Firebase Firestore**

[https://firebase.google.com/docs/firestore](https://firebase.google.com/docs/firestore)

Stores all message documents (text, coordinates, geohash, timestamps, echo data, mood colors), the global message counter, and per-user stamp collections.

**Firebase Storage**

[https://firebase.google.com/docs/storage](https://firebase.google.com/docs/storage)

Stores uploaded images attached to messages. Files are uploaded here and the resulting download URLs are saved in Firestore message documents.

**Firebase Hosting**

[https://firebase.google.com/docs/hosting]([https://firebase.google.com/docs/hosting])

Hosts the built static SvelteKit app, providing the live `.web.app` deployment URL.

**Firebase Authentication (Anonymous)**

[https://firebase.google.com/docs/auth/web/anonymous-auth](https://firebase.google.com/docs/auth/web/anonymous-auth)

Gives every device a persistent, anonymous user ID without requiring registration. Used to link messages to a device for the archive page and stamps, preserving the app's anonymous design.

**ngeohash**

[https://www.npmjs.com/package/ngeohash](https://www.npmjs.com/package/ngeohash)

Converts coordinates to geohash strings and back (`encode`, `decode_bbox`). This powers the proximity-based "nearby messages" queries.

**@mediapipe/tasks-vision (BlazeFace)**

[https://ai.google.dev/edge/mediapipe/solutions/vision/face_detector/web_js](https://ai.google.dev/edge/mediapipe/solutions/vision/face_detector/web_js)

Google's face detection model. Used to scan images before upload and block any containing faces.

**lucide-react**

[https://lucide.dev/](https://lucide.dev/)

Outline icon set used throughout the UI( navigation bar icons, stamp icons, and other interface elements). 

**qrcode**

[https://www.npmjs.com/package/qrcode](https://www.npmjs.com/package/qrcode)

Generates QR codes for the share feature, allowing any message to be turned into a scannable code that links back to that specific message.

**SvelteKit / Svelte 5**

[https://svelte.dev/docs/kit/introduction](https://svelte.dev/docs/kit/introduction)

The application framework, Svelte 5's runes mode (`$state`, `$derived`, `$effect`, `$props`) is used throughout for reactive state management, and SvelteKit provides routing (map page, archive page) and the build pipeline.

**@sveltejs/adapter-static**

[https://www.npmjs.com/package/@sveltejs/adapter-static](https://www.npmjs.com/package/@sveltejs/adapter-static)

Configures SvelteKit to build as a fully static site, which is what allows the app to be deployed to Firebase Hosting.

- Claude (coding assistance, debugging, feature implementation)
- Copilot (coding assistance)
  - Further documentation on AI Prompts along with additional work documentation can be found in the following notion link [https://quilted-recorder-3b4.notion.site/Final-Project-ongoing-documentation-370f941b5e518013a8a7dd703d7dbe22?source=copy_link](https://quilted-recorder-3b4.notion.site/Final-Project-ongoing-documentation-370f941b5e518013a8a7dd703d7dbe22?source=copy_link)
- Course notes (ID 30011)
- Referenced code and materials from my own Individual project from earlier in the semester