CommonSight
A role-based community engagement platform that connects neighborhood reporting, mapping, campaigns, stories, events, and direct messaging.
Problem
Community insight is often fragmented across separate reporting, communication, and planning tools. The goal was to create a single workflow where neighborhood observations can become coordinated action.
Project Snapshot
- Platform: iOS (SwiftUI + Firebase)
- Type: Civic/community collaboration app
- Focus: Role-based workflows, shared data, community storytelling
- Engagement: Internship project supporting a small business owner's investor demo
- Team: Small collaboration
- Timeline: Multi-phase design system initiative
Demo Video
Demo video: CommonSight app walkthrough.
Role
Product designer and iOS engineer responsible for information architecture, role-based flow design, SwiftUI implementation, and Firebase integration.
My Contributions
- Implemented app-level navigation state with gated flows (splash, onboarding, invite/login, profile setup, main experience)
- Built role-based access patterns across modules for residents, partners, and admins
- Developed Firestore-backed workflows for observations, campaigns, users, and thread notifications
- Implemented direct messaging thread UX with unread indicators and attachment support
- Designed and shipped the narrative pipeline that transforms selected observations into story cards
Constraints
Support multiple user roles and permissions in one product while keeping workflows clear enough for everyday community use and reliable enough for investor-facing demos.
Key Decisions
- Structured the product into five core tabs: Dashboard, Commons Atlas, Groundtruth, Narrative Alchemy, and Calendar
- Connected authentication and invite-code logic to role/community-aware app behavior
- Used Firestore listeners for near real-time updates to campaigns, users, and message notifications
- Kept map/report/story modules loosely coupled through shared data models and environment-driven view models
Core Features
- Groundtruth observation capture with category tagging, geolocation context, and optional photo evidence
- Commons Atlas map overlay for community assets and observations with filter toggles and detail sheets
- Narrative Alchemy story generation from selected observations and campaign orchestration tools
- Community-scoped direct messaging with unread thread indicators and attachment support
- Dashboard summaries for upcoming events, module entry points, and recent activity context
Technical Highlights
- FirebaseAuth + Firestore integration with normalized community scoping for multi-user collaboration
- Document listeners for campaigns/users/threads to keep UI in sync without manual refresh loops
- Stable thread ID strategy and local read-state handling for reliable unread badge behavior
- Typed model layer (`User`, `Campaign`, `Observation`, `Message`, `StoryCard`) for safer feature composition
Code Highlights
struct Observation: Identifiable, Codable {
let id: UUID
var category: ObservationCategory
var title: String
var description: String
var location: Coordinate
var locationName: String
var timestamp: Date
var userId: String
var userName: String
var photoURL: String?
var status: ObservationStatus
struct Coordinate: Codable {
let latitude: Double
let longitude: Double
}
enum ObservationCategory: String, Codable, CaseIterable {
case hazard, vacantLot, communitySpace, business, opportunity
}
enum ObservationStatus: String, Codable {
case active, reviewed, resolved
}
}
func startObservationsListener(communityCode: String, isDemoMode: Bool = false) {
observationsListener = db.collection("communities")
.document(communityCode)
.collection("observations")
.addSnapshotListener { [weak self] snapshot, error in
guard let self else { return }
if let error {
self.errorMessage = "Failed to load observations."
self.observations = isDemoMode ? DemoData.allDemoObservations() : []
return
}
guard let snapshot else { return }
let fetched = snapshot.documents.compactMap { document in
try? document.data(as: Observation.self)
}
self.observations = isDemoMode ? fetched + DemoData.allDemoObservations() : fetched
}
}
func startListening(communityCode: String, currentUserId: String) {
listener = db.collection("communities")
.document(communityCode)
.collection("threads")
.addSnapshotListener { [weak self] snapshot, _ in
guard let self, let snapshot else { return }
var unread: Set = []
for document in snapshot.documents {
let data = document.data()
let threadId = document.documentID
let participantIds = data["participantIds"] as? [String] ?? []
guard participantIds.contains(currentUserId) else { continue }
let senderId = data["lastMessageSenderId"] as? String
if senderId == currentUserId { continue }
let lastReadAt = (data["lastReadAt"] as? [String: Timestamp])?[currentUserId]
let lastMessageAt = data["lastMessageAt"] as? Timestamp
if let lastReadAt, let lastMessageAt, lastReadAt.dateValue() < lastMessageAt.dateValue() {
unread.insert(threadId)
}
}
self.unreadThreadIds = unread
}
}
func generateStoryCard(from observation: Observation) -> StoryCard {
let context = "Observation: \(observation.title)\nCategory: \(observation.category.rawValue)\nLocation: \(observation.locationName)\nDescription: \(observation.description)"
let callToAction = "Take action: \(observation.status == .active ? "Report or resolve" : "Reviewed")"
return StoryCard(
id: UUID(),
context: context,
callToAction: callToAction,
coalition: "Neighborhood Coalition",
createdAt: Date()
)
}
Outcome
CommonSight evolved into a unified civic workflow rather than separate utilities. The app demonstrates full-stack product thinking on iOS—combining data capture, geospatial context, collaboration, and campaign storytelling into one coherent experience.
Key Screens
Next Iteration
Planned next steps include richer analytics for campaign impact, stronger moderation workflows, and deeper offline resilience for field reporting scenarios.