Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chat Rooms

Small group chat app with three fixed rooms (General, Random, Dev). Users send text and short voice messages. A Node backend sends Expo push notifications so tapping a notification opens the correct room.

Stack

  • Frontend: Expo (React Native) + TypeScript + Redux Toolkit + react-native-paper
  • Data: Firebase Firestore (messages) + Firebase Storage (audio)
  • Push: expo-notifications + Expo Push API via Express backend
  • Build: EAS Build (Android APK)

Repository layout

root/
  frontend/   # Expo app
  backend/    # Express push relay
  README.md
  firestore.rules
  storage.rules

Prerequisites

  1. Node.js LTS
  2. Expo account (npm i -g eas-cli then eas login)
  3. Firebase project with Firestore and Storage enabled
  4. Physical Android device (push does not fully work in Expo Go for production-like FCM flows; use an EAS APK)
  5. FCM credentials configured in EAS (eas credentials) for Android push

Setup

1. Firebase

  1. Create a Firebase project.
  2. Enable Firestore and Storage.
  3. Deploy rules from this repo (or paste them in the console):
firebase deploy --only firestore:rules,storage
  1. Create a Web app in Firebase and copy the config values.

2. Backend

cd backend
cp .env.example .env
npm install
npm run dev

Fill backend/.env:

  • PORT (optional, default 4000)
  • Firebase Admin (required for user registration add-on): either
    • GOOGLE_APPLICATION_CREDENTIALS — path to a service account JSON file, or
    • FIREBASE_SERVICE_ACCOUNT_JSON — full JSON string (useful on Render)

Create a service account in Firebase Console → Project settings → Service accounts → Generate new private key. Keep the file out of git.

Backend listens on http://0.0.0.0:4000.

Endpoints:

  • GET /health
  • POST /register{ "token": "<expo-push-token>", "senderName": "Ada", "userId?", "notificationPreferencesByRoom?": { "general": "mentions", "random": "all" } }
  • POST /notify{ "roomId", "roomName", "senderName", "senderToken", "messageType": "text"|"voice", "text?", "mentionedUserIds?" }
  • POST /users/lookup{ "email" }{ found, id?, email?, name?, avatarId?, suggestions }
  • POST /users/register{ "email", "name", "avatarId" }{ id, email, name, avatarId, suggestions } (or 409 if name taken)
  • GET /users/ids{ "ids": ["..."], "profiles": [{ "id", "avatarId" }] } (for read-receipt ticks + avatars)

Device tokens are stored in memory (restarts clear them), as allowed by the assignment. Registered users are stored in Firestore users.

3. Frontend

cd frontend
cp .env.example .env

Fill .env:

  • EXPO_PUBLIC_API_URL — your PC LAN IP, e.g. http://192.168.1.10:4000 (not localhost on a physical device)
  • Firebase EXPO_PUBLIC_FIREBASE_* values from the Firebase console
npm install
npx expo start

For day-to-day chat UI work you can use Expo Go.

Important: From SDK 53, Android remote push does not work in Expo Go (the app will skip push APIs there). To test notifications you must use an EAS development or preview APK (eas build -p android --profile preview or a development build), with FCM credentials configured.

4. EAS / push (required for real notification testing)

cd frontend
eas init

Replace placeholders in app.json:

  • extra.eas.projectId
  • owner

Upload FCM V1 credentials:

eas credentials

Build an installable APK:

eas build -p android --profile preview

Install the APK on a fresh Android device.

How the notification flow works

  1. App requests permission and obtains an Expo push token.
  2. App registers the token with POST /register.
  3. After a message is written to Firestore, the sender calls POST /notify.
  4. Backend fans out Expo push messages to all tokens except the sender.
  5. Notification data.roomId is used for deep linking.
  6. Tap handling covers:
    • App open (foreground listener)
    • Background
    • Fully closed / cold start via getLastNotificationResponseAsync
  7. Navigation resets to Rooms → Chat(roomId) so Back returns to the rooms list.

Cold start differs because the JS runtime was not running; the last notification response must be read after navigation mounts.

Assignment checklist

  • Text messages send and appear in the correct room
  • Voice recording works; mic denial does not crash
  • Voice messages upload and play back
  • Playing one voice message stops any other
  • Notification arrives (app open / background / killed)
  • Tap opens the correct room in all three states
  • Back from notification-opened chat lands on rooms list
  • Message list handles a few hundred messages without lag
  • Keyboard does not cover the input
  • APK installs on a fresh device

Demo video outline (5–10 min)

  1. Enter display name; open a room; send text; show it on a second device
  2. Record and send a voice message; play it; start another to show single playback
  3. Deny mic permission (or revoke) and show graceful handling
  4. Notification while app open → tap → correct room
  5. Background the app → notification → tap → correct room
  6. Force-stop / swipe away → notification → cold start → correct room + Back to rooms
  7. Brief walkthrough of notificationService + useNotificationNavigation + backend /notify
  8. Any add-ons (registration, avatars, edit/delete, read receipts, typing, presence below)

What isn’t finished / next steps

  • Replace demo Firebase rules with authenticated access
  • Persist push tokens (DB) if the backend must survive restarts in production
  • Host the backend publicly (Railway/Fly/Render) instead of LAN IP
  • Complete EAS projectId / FCM credentials on your Expo account before final APK testing
  • Add real auth (password / OTP) if registration leaves demo mode

Add-ons

Passwordless email registration

What: Users enter an email + unique display name before joining rooms. Returning emails autofill the registered name (editable). Fancy name suggestions are always shown.

Why: Personalizes the app and avoids display-name collisions (own-message bubbles and chat identity).

How it works:

  • Frontend NameScreenPOST /users/lookup and POST /users/register
  • Backend stores { id, email, name } in Firestore users (plus emailLower / nameLower for uniqueness)
  • Email format is validated on client and server; names are globally unique (case-insensitive)
  • Changing a name updates the profile only; older messages keep the previous senderName

Security caveat (demo only): There is no password or OTP. Anyone who knows an email can open as that user. Do not use this pattern for production.

Setup: Add Firebase Admin credentials to backend/.env (see Backend setup above). Client Firestore rules deny direct access to users; only the Admin SDK writes.

Edit & delete own messages

What: Long-press your own chat bubble to edit a text message or permanently delete a text/voice message.

Why: Lets users fix typos and remove mistaken sends without leaving leftover “deleted” placeholders.

How it works:

  • Own messages are matched by senderId (when present) with a fallback to senderName for older messages
  • New sends store senderId from the registered user profile
  • Choosing Edit loads the text into the chat input; Send saves the change and Cancel (×) exits edit mode
  • Text edit updates Firestore text and sets editedAt (shown as “edited” on the bubble)
  • Hard delete removes the Firestore document; voice messages also attempt to delete the Storage audio file afterward
  • Room list previews update automatically via the existing latest-message listener

Security caveat (demo only): Chat Firestore/Storage write rules remain open for the three rooms (same as the rest of the chat demo). Do not treat client-side ownership checks as real authorization.

Read receipts, ticks, and “New messages”

What: Own messages show WhatsApp-style ticks (single gray = sent, double blue = read by every other registered user). Re-entering a room with unread traffic shows a “New messages” separator. The list also auto-scrolls to the latest message.

Why: Makes delivery/read state visible in a multi-user room without adding a full chat backend.

How it works:

  • Each viewer writes rooms/{roomId}/reads/{userId}.lastReadAt while the chat is open (throttled) and on leave
  • GET /users/ids loads all registered accounts (clients cannot read the private users collection)
  • Ticks turn double-blue only when every other registered user has lastReadAt >= message.createdAt
  • On enter, the previous lastReadAt is captured before updating, and used to place the separator
  • Solo account (no other registered users) shows blue ticks immediately (vacuous “read by all”)

Caveat: If a new user registers later, previously blue ticks can drop back to single until that user also opens and reads the room.

Security caveat (demo only): reads docs use the same open room write rules as messages. Redeploy firestore.rules after pulling this change.

Predefined avatars

What: On registration, users pick one of 9 original comic-hero avatars. The choice shows on incoming chat bubbles for everyone.

Why: Personalizes the room without image uploads or Storage usage.

How it works:

  • Avatar art lives only on the frontend (constants/avatars.ts + UserAvatar) — original hero faces, not third-party IP
  • Firestore users stores avatarId ("1""9") with the profile
  • Returning users get their saved avatar from lookup; they can change it before Continue
  • Chat resolves others’ avatars via GET /users/idsprofiles using message.senderId (legacy messages without senderId fall back to initials)

Typing indicators

What: While someone is composing in a room, others see “Ada is typing…” above the composer.

Why: Makes the chat feel live in a two-device demo without extra backend work.

How it works:

  • Composer keystrokes write rooms/{roomId}/typing/{userId} with displayName + updatedAt (throttled ~900ms)
  • Idle (~2.5s), send, leave room, or clearing the input deletes the typing doc
  • Listeners ignore their own doc and treat entries older than ~4s as stale (client TTL)
  • Labels: one name, two names, or “Several people are typing…”

Security caveat (demo only): Typing docs use the same open room write rules. Redeploy firestore.rules after pulling this change.

Online / offline presence

What: Chat shows a people strip of other users with a green online dot or a red offline dot plus relative last seen (2m ago).

Why: Demonstrates real-time presence with Firestore and AppState.

How it works:

  • While the app is foregrounded, the client writes presence/{userId} (isOnline: true) and heartbeats about every 25s
  • Background / leave clears to offline and sets lastSeenAt
  • Chat subscribes to all presence docs (excludes self); stale online heartbeats (>45s) render as offline
  • Redeploy firestore.rules so presence/{userId} is readable/writable

@mentions

What: Type @ in the composer to tag someone (e.g. @Ada). Suggestions come from the presence people list; mentions are highlighted in bubbles.

Why: Makes addressing people in a group room explicit and easy to demo.

How it works:

  • @ at the end of the input opens filtered suggestions (excludes self)
  • Selecting a name inserts @DisplayName
  • On send/edit, matched mentions are stored as mentionedUserIds / mentionedNames
  • Bubble text highlights known @Name spans

Chat notification preference

What: Chat header menu (⋮) → Chat notification settings — choose All messages or Only mentions for that room.

Why: Lets users cut noise in busy rooms while still getting pinged when tagged, without affecting other rooms.

How it works:

  • Preference is stored per room in Redux (persisted) and sent with POST /register as notificationPreferencesByRoom
  • On send, POST /notify includes roomId + mentionedUserIds
  • Backend applies that device’s preference for the message’s room only (mentions → skip unless userId is mentioned)
  • Rooms with no saved preference default to all

Time spent

Roughly ~18 hours total:

Area Approx. time
Setup, Expo project structure, env / Firebase wiring ~2.5 h
Frontend screens (name, rooms, chat) + Redux + Paper UI ~4 h
Voice record / upload / playback (expo-audio, Storage quirks) ~3 h
Express backend (/register, /notify, Expo Push) ~2 h
Android FCM + EAS credentials + native / USB debug build ~2.5 h
Notification deep links + device testing (open / background / killed) ~2 h
Add-ons / polish (e.g. mentions, per-room prefs, Render host) + README ~2 h

Estimates only;

Scripts

Location Command Purpose
frontend npm start Expo dev server
frontend eas build -p android --profile preview APK build
backend npm run dev Start push API with reload
backend npm start Start push API

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages