mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Update
This commit is contained in:
1 parent
358c2be0ee
commit
b10eae1bf3
26 files changed
+2509
-53
No files matched your search
@@ -0,0 +1,293 @@
|
||||
# Match & Notification System
|
||||
|
||||
This document explains how matches are detected, stored, and how push notifications are triggered.
|
||||
|
||||
## Overview
|
||||
|
||||
A **match** occurs when both partners in a couple answer a question positively (not "no"). Matches unlock a chat thread for discussing that topic.
|
||||
|
||||
## Match Types
|
||||
|
||||
| Type | Partner A | Partner B | Description |
|
||||
|------|-----------|-----------|-------------|
|
||||
| `yes_yes` | Yes | Yes | Both enthusiastic |
|
||||
| `yes_maybe` | Yes | Maybe | One enthusiastic, one open |
|
||||
| `maybe_maybe` | Maybe | Maybe | Both open to exploring |
|
||||
|
||||
**No Match Cases:**
|
||||
- Either partner answers "No" → No match created
|
||||
- Only one partner has answered → No match yet (waiting)
|
||||
|
||||
## Match Detection Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ submit-response Edge Function │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. Validate user JWT │
|
||||
│ 2. Get user's couple_id from profiles │
|
||||
│ 3. UPSERT response (user_id, question_id, answer) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 4. Query partner's response for same question │
|
||||
│ SELECT answer FROM responses │
|
||||
│ WHERE couple_id = ? AND question_id = ? AND user_id != ? │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┴───────────────┐
|
||||
▼ ▼
|
||||
Partner answered? No partner response
|
||||
│ │
|
||||
▼ ▼
|
||||
calculateMatch(a1, a2) Return { response }
|
||||
│ (no match yet)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ function calculateMatch(a1, a2): │
|
||||
│ if (a1 === "no" || a2 === "no") return null │
|
||||
│ if (a1 === "yes" && a2 === "yes") return "yes_yes" │
|
||||
│ if (a1 === "maybe" && a2 === "maybe") return "maybe_maybe"│
|
||||
│ return "yes_maybe" │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┴───────────────┐
|
||||
▼ ▼
|
||||
Match type found null (no match)
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────┐ Return { response }
|
||||
│ UPSERT match record │
|
||||
│ (couple_id, question_id) │
|
||||
└────────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ Invoke send-notification │
|
||||
│ { couple_id, match_id } │
|
||||
└────────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
Return { response, match }
|
||||
```
|
||||
|
||||
## Match Storage
|
||||
|
||||
```sql
|
||||
CREATE TABLE public.matches (
|
||||
id UUID PRIMARY KEY,
|
||||
couple_id UUID NOT NULL REFERENCES couples(id),
|
||||
question_id UUID NOT NULL REFERENCES questions(id),
|
||||
match_type match_type NOT NULL, -- yes_yes, yes_maybe, maybe_maybe
|
||||
is_new BOOLEAN DEFAULT TRUE, -- For notification badge
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(couple_id, question_id) -- One match per question per couple
|
||||
);
|
||||
```
|
||||
|
||||
**UPSERT Behavior:**
|
||||
Matches use `onConflict: "couple_id,question_id"` so if a user changes their answer, the match type updates rather than creating duplicates.
|
||||
|
||||
## Push Notifications
|
||||
|
||||
### Match Notification (`send-notification`)
|
||||
|
||||
Triggered when a new match is created.
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{ "couple_id": "uuid", "match_id": "uuid" }
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
1. Fetch all profiles in couple with push tokens
|
||||
2. Build notification for both partners:
|
||||
```javascript
|
||||
{
|
||||
to: profile.push_token,
|
||||
title: "It's a match! 💕",
|
||||
body: "You and your partner matched on something new!",
|
||||
sound: "default",
|
||||
data: { match_id, type: "match" }
|
||||
}
|
||||
```
|
||||
3. Send via Expo Push API
|
||||
|
||||
**Privacy:** Notification content is generic - doesn't reveal the question topic.
|
||||
|
||||
### Message Notification (`send-message-notification`)
|
||||
|
||||
Triggered when a chat message is sent.
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{ "match_id": "uuid", "sender_id": "uuid" }
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
1. Find the match's couple_id
|
||||
2. Get recipient's push token (partner, not sender)
|
||||
3. Build notification:
|
||||
```javascript
|
||||
{
|
||||
to: recipient.push_token,
|
||||
title: "New message",
|
||||
body: "Your partner sent you a message",
|
||||
sound: "default",
|
||||
data: { type: "message", match_id, message_id }
|
||||
}
|
||||
```
|
||||
4. Send via Expo Push API
|
||||
|
||||
**Privacy:** Message content not included in notification.
|
||||
|
||||
## Client State Management
|
||||
|
||||
### Match Store (`src/store/index.ts`)
|
||||
|
||||
```typescript
|
||||
interface MatchState {
|
||||
matches: Match[];
|
||||
newMatchesCount: number;
|
||||
fetchMatches: () => Promise<void>;
|
||||
markAsSeen: (matchId: string) => Promise<void>;
|
||||
markAllAsSeen: () => Promise<void>;
|
||||
addMatch: (match: Match) => void;
|
||||
clearMatches: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### Fetching Matches
|
||||
|
||||
The `fetchMatches` function:
|
||||
1. Queries matches with joined question data
|
||||
2. Fetches all responses to determine who answered first (for two-part display)
|
||||
3. Counts unread messages per match
|
||||
4. Calculates new matches count (where `is_new = true`)
|
||||
|
||||
```typescript
|
||||
const { data: matches } = await supabase
|
||||
.from("matches")
|
||||
.select(`*, question:questions(*)`)
|
||||
.order("created_at", { ascending: false });
|
||||
```
|
||||
|
||||
### Match Display
|
||||
|
||||
The matches screen shows:
|
||||
- Question text (user's version for two-part questions)
|
||||
- Partner's question text (if two-part)
|
||||
- Match type badge (`YES + YES` or `YES + MAYBE`)
|
||||
- Unread message indicator
|
||||
- Date
|
||||
|
||||
**Two-Part Question Display Logic:**
|
||||
```typescript
|
||||
// Determine which text each person saw based on response timestamps
|
||||
if (userResponse.created_at > partnerResponse.created_at) {
|
||||
userText = question.partner_text; // User was second
|
||||
partnerText = question.text; // Partner was first
|
||||
}
|
||||
```
|
||||
|
||||
## Chat System
|
||||
|
||||
Each match unlocks a chat thread stored in `messages` table:
|
||||
|
||||
```sql
|
||||
CREATE TABLE public.messages (
|
||||
id UUID PRIMARY KEY,
|
||||
match_id UUID NOT NULL REFERENCES matches(id),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
content TEXT,
|
||||
media_path TEXT, -- Storage path for images
|
||||
read_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ,
|
||||
CONSTRAINT content_or_media CHECK (content IS NOT NULL OR media_path IS NOT NULL)
|
||||
);
|
||||
```
|
||||
|
||||
### Real-Time Chat
|
||||
|
||||
Messages use Supabase Realtime:
|
||||
```sql
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE messages;
|
||||
```
|
||||
|
||||
### Message Read Tracking
|
||||
|
||||
- `read_at` is `NULL` for unread messages
|
||||
- Updated when recipient opens chat thread
|
||||
- Used to calculate unread counts and badges
|
||||
|
||||
## Notification Flow Diagram
|
||||
|
||||
```
|
||||
User A swipes "Yes"
|
||||
│
|
||||
▼
|
||||
submit-response
|
||||
│
|
||||
├──▶ Save response
|
||||
│
|
||||
├──▶ Check partner response
|
||||
│ │
|
||||
│ ▼
|
||||
│ Partner said "Yes"?
|
||||
│ │
|
||||
│ ▼
|
||||
│ Create match (yes_yes)
|
||||
│ │
|
||||
└─────────┼──▶ Invoke send-notification
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Expo Push Service │
|
||||
└─────────────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
▼ ▼
|
||||
User A Phone User B Phone
|
||||
"It's a match!" "It's a match!"
|
||||
```
|
||||
|
||||
## RLS Policies
|
||||
|
||||
Users can only see matches for their couple:
|
||||
|
||||
```sql
|
||||
CREATE POLICY "Users can view their couple matches"
|
||||
ON public.matches FOR SELECT
|
||||
USING (
|
||||
couple_id IN (
|
||||
SELECT couple_id FROM profiles WHERE id = auth.uid()
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Badge Management
|
||||
|
||||
### New Match Badge
|
||||
|
||||
- Set to `is_new = TRUE` on match creation
|
||||
- Updated to `FALSE` when user views matches screen
|
||||
- Count displayed on tab bar badge
|
||||
|
||||
```typescript
|
||||
// On matches screen focus
|
||||
useEffect(() => {
|
||||
if (matches.length > 0) {
|
||||
markAllAsSeen(); // Clears is_new flags
|
||||
}
|
||||
}, [matches.length]);
|
||||
```
|
||||
|
||||
### Unread Message Badge
|
||||
|
||||
- Counted per match from messages where `read_at IS NULL`
|
||||
- Displayed as bubble on match card
|
||||
- Cleared when entering chat thread
|
||||
Reference in new issue
Block a user