mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Add release notes and fix answer gap deadlock
- Add mobile release notes for v1.0.1 - Fix answer gap deadlock with NET gap calculation - Randomize question order for variety - Dismiss keyboard when match modal appears - Add keyboard dismiss on chat scroll - Pin EAS build node version
This commit is contained in:
1 parent
df955812fe
commit
a7a13c9614
9 files changed
+618
-5
No files matched your search
@@ -25,7 +25,7 @@
|
|||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@sauci/shared": "workspace:*",
|
"@sauci/shared": "file:../../packages/shared",
|
||||||
"@supabase/supabase-js": "^2.39.0",
|
"@supabase/supabase-js": "^2.39.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Sauci",
|
"name": "Sauci",
|
||||||
"slug": "sauci",
|
"slug": "sauci",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"scheme": "app.sauci",
|
"scheme": "app.sauci",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": false,
|
"supportsTablet": false,
|
||||||
"bundleIdentifier": "com.sauci.app",
|
"bundleIdentifier": "com.sauci.app",
|
||||||
"buildNumber": "25",
|
"buildNumber": "30",
|
||||||
"usesAppleSignIn": true,
|
"usesAppleSignIn": true,
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"UIBackgroundModes": [
|
"UIBackgroundModes": [
|
||||||
|
|||||||
@@ -5,13 +5,16 @@
|
|||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"development": {
|
"development": {
|
||||||
|
"node": "20.10.0",
|
||||||
"developmentClient": true,
|
"developmentClient": true,
|
||||||
"distribution": "internal"
|
"distribution": "internal"
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
|
"node": "20.10.0",
|
||||||
"distribution": "internal"
|
"distribution": "internal"
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
|
"node": "20.10.0",
|
||||||
"autoIncrement": true,
|
"autoIncrement": true,
|
||||||
"env": {
|
"env": {
|
||||||
"EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co",
|
"EXPO_PUBLIC_SUPABASE_URL": "https://ckjcrkjpmhqhiucifukx.supabase.co",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.0</string>
|
<string>1.0.1</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>25</string>
|
<string>30</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Sauci Mobile Release Notes
|
||||||
|
|
||||||
|
## v1.0.1 (Build 30) - January 2025
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
- **Match Archiving** - Archive old matches to keep your active list focused
|
||||||
|
- **Daily Swipe Limits** - Pace your discovery with daily question limits
|
||||||
|
- **Intensity Preferences** - Set your comfort level for question intensity (1-5 scale)
|
||||||
|
- **Infinite Scroll** - Smoother browsing through Matches and Answers with infinite scroll
|
||||||
|
- **Chat Date Separators** - Easily see when conversations happened
|
||||||
|
- **Lifecycle Notifications** - Get reminded to check in with your partner
|
||||||
|
- **Message Reporting** - Report inappropriate messages for review
|
||||||
|
- **Account Deletion** - Full account deletion option in settings
|
||||||
|
- **Reset Progress** - Start fresh by resetting your question progress
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- **Enhanced Swipe UI** - Smoother swiping experience with visual refinements
|
||||||
|
- **Improved Chat Layout** - Match card moved into message list for cleaner design
|
||||||
|
- **Keyboard Behavior** - Chat header collapses when keyboard opens for more screen space
|
||||||
|
- **Profile Settings** - New appearance and notification preferences
|
||||||
|
- **Premium Design** - Updated subscription card with gold styling
|
||||||
|
- **Version Display** - App version now visible in settings
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- Fixed subscription status incorrectly showing "Via partner's" after direct purchase
|
||||||
|
- Fixed Android navigation issues
|
||||||
|
- Various performance improvements and stability fixes
|
||||||
|
|
||||||
|
### Privacy
|
||||||
|
- Removed partner answered indicator for enhanced privacy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.0 (Build 25) - January 8, 2025
|
||||||
|
|
||||||
|
Initial release.
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
Pressable,
|
Pressable,
|
||||||
Animated,
|
Animated,
|
||||||
|
Keyboard,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
@@ -106,6 +107,9 @@ export const MatchNotificationModal: React.FC<Props> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
// Dismiss keyboard so it doesn't block the modal buttons
|
||||||
|
Keyboard.dismiss();
|
||||||
|
|
||||||
// Reset animations
|
// Reset animations
|
||||||
modalScale.setValue(0.8);
|
modalScale.setValue(0.8);
|
||||||
modalOpacity.setValue(0);
|
modalOpacity.setValue(0);
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ const ChatMessagesComponent: React.FC<ChatMessagesProps> = ({
|
|||||||
style={styles.messageList}
|
style={styles.messageList}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
|
keyboardDismissMode="on-drag"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
-- Fix answer gap deadlock by calculating NET gap instead of independent gaps
|
||||||
|
-- Also add ORDER BY to get_recommended_questions to prioritize partner-answered questions
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Part 1: Fix get_answer_gap_status() to calculate NET gap
|
||||||
|
-- ============================================================================
|
||||||
|
--
|
||||||
|
-- Problem: Both partners can be "ahead" simultaneously when they answer different questions
|
||||||
|
-- Example: User A answers Q1-Q10, User B answers Q11-Q20
|
||||||
|
-- - Old: Both have gap=10, both blocked (deadlock!)
|
||||||
|
-- - New: Both have net_gap=0, neither blocked
|
||||||
|
--
|
||||||
|
-- NET gap = (questions I answered that partner hasn't) - (questions partner answered that I haven't)
|
||||||
|
-- Only positive values mean user is truly ahead
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_answer_gap_status()
|
||||||
|
RETURNS TABLE(
|
||||||
|
unanswered_by_partner integer,
|
||||||
|
threshold integer,
|
||||||
|
is_blocked boolean
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
v_couple_id UUID;
|
||||||
|
v_partner_id UUID;
|
||||||
|
v_my_unanswered integer;
|
||||||
|
v_partner_unanswered integer;
|
||||||
|
v_net_gap integer;
|
||||||
|
v_threshold integer;
|
||||||
|
BEGIN
|
||||||
|
v_user_id := auth.uid();
|
||||||
|
|
||||||
|
-- Get user's couple_id
|
||||||
|
SELECT p.couple_id INTO v_couple_id
|
||||||
|
FROM profiles p
|
||||||
|
WHERE p.id = v_user_id;
|
||||||
|
|
||||||
|
-- No couple = no blocking needed
|
||||||
|
IF v_couple_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT 0, 0, false;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get partner's ID
|
||||||
|
SELECT p.id INTO v_partner_id
|
||||||
|
FROM profiles p
|
||||||
|
WHERE p.couple_id = v_couple_id AND p.id != v_user_id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- No partner = no blocking needed
|
||||||
|
IF v_partner_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT 0, 0, false;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get threshold from app_config
|
||||||
|
SELECT COALESCE(ac.answer_gap_threshold, 10) INTO v_threshold
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If threshold is 0, feature is disabled
|
||||||
|
IF v_threshold = 0 THEN
|
||||||
|
RETURN QUERY SELECT 0, 0, false;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Calculate NET gap to prevent deadlocks
|
||||||
|
WITH enabled_packs AS (
|
||||||
|
-- Get packs that the couple has explicitly enabled
|
||||||
|
SELECT cp.pack_id
|
||||||
|
FROM couple_packs cp
|
||||||
|
WHERE cp.couple_id = v_couple_id AND cp.enabled = true
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
-- Questions I answered that partner hasn't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_user_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_partner_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer,
|
||||||
|
-- Questions partner answered that I haven't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_partner_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_user_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer
|
||||||
|
INTO v_my_unanswered, v_partner_unanswered
|
||||||
|
FROM responses r
|
||||||
|
INNER JOIN questions q ON q.id = r.question_id
|
||||||
|
WHERE r.couple_id = v_couple_id
|
||||||
|
AND r.user_id IN (v_user_id, v_partner_id)
|
||||||
|
AND EXISTS (SELECT 1 FROM enabled_packs)
|
||||||
|
AND q.pack_id IN (SELECT pack_id FROM enabled_packs);
|
||||||
|
|
||||||
|
-- Net gap: only positive values mean user is truly ahead
|
||||||
|
-- If partner has answered more, user's net gap is 0 (they're behind or even)
|
||||||
|
v_net_gap := GREATEST(0, v_my_unanswered - v_partner_unanswered);
|
||||||
|
|
||||||
|
-- Return net gap (displayed as "unanswered_by_partner" for backwards compatibility)
|
||||||
|
RETURN QUERY SELECT v_net_gap, v_threshold, (v_net_gap >= v_threshold);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION get_answer_gap_status() IS 'Returns answer gap status using NET gap calculation. Prevents deadlocks when both partners answer different questions.';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Part 2: Update get_recommended_questions() to prioritize partner-answered questions
|
||||||
|
-- ============================================================================
|
||||||
|
--
|
||||||
|
-- Add ORDER BY to ensure:
|
||||||
|
-- 1. Questions partner has already answered come first (helps resolve gaps)
|
||||||
|
-- 2. Lower intensity questions shown first within each group
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_recommended_questions(target_pack_id UUID DEFAULT NULL)
|
||||||
|
RETURNS TABLE(
|
||||||
|
id UUID,
|
||||||
|
text TEXT,
|
||||||
|
partner_text TEXT,
|
||||||
|
is_two_part BOOLEAN,
|
||||||
|
pack_id UUID,
|
||||||
|
intensity INTEGER,
|
||||||
|
partner_answered BOOLEAN,
|
||||||
|
allowed_couple_genders TEXT[],
|
||||||
|
target_user_genders TEXT[]
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
v_couple_id UUID;
|
||||||
|
v_partner_id UUID;
|
||||||
|
v_user_gender TEXT;
|
||||||
|
v_partner_gender TEXT;
|
||||||
|
v_couple_type TEXT;
|
||||||
|
v_has_premium BOOLEAN;
|
||||||
|
v_gap_threshold INTEGER;
|
||||||
|
v_my_unanswered INTEGER;
|
||||||
|
v_partner_unanswered INTEGER;
|
||||||
|
v_net_gap INTEGER;
|
||||||
|
v_user_max_intensity INTEGER;
|
||||||
|
v_daily_limit INTEGER;
|
||||||
|
v_daily_count INTEGER;
|
||||||
|
v_today_start TIMESTAMPTZ;
|
||||||
|
v_tomorrow_start TIMESTAMPTZ;
|
||||||
|
BEGIN
|
||||||
|
v_user_id := auth.uid();
|
||||||
|
|
||||||
|
SELECT p.couple_id, p.gender, COALESCE(p.max_intensity, 5)
|
||||||
|
INTO v_couple_id, v_user_gender, v_user_max_intensity
|
||||||
|
FROM public.profiles p
|
||||||
|
WHERE p.id = v_user_id;
|
||||||
|
|
||||||
|
IF v_couple_id IS NULL THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT p.id, p.gender INTO v_partner_id, v_partner_gender
|
||||||
|
FROM public.profiles p
|
||||||
|
WHERE p.couple_id = v_couple_id AND p.id != v_user_id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- Check premium access (used for both answer gap and daily limit)
|
||||||
|
v_has_premium := public.has_premium_access(v_user_id);
|
||||||
|
|
||||||
|
-- Check daily response limit (only when not browsing a specific pack and user is non-premium)
|
||||||
|
IF v_partner_id IS NOT NULL AND target_pack_id IS NULL AND NOT v_has_premium THEN
|
||||||
|
-- Get daily limit from app_config
|
||||||
|
SELECT COALESCE(ac.daily_response_limit, 0) INTO v_daily_limit
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If daily limit > 0, check if user has reached it
|
||||||
|
IF v_daily_limit > 0 THEN
|
||||||
|
-- Calculate UTC day boundaries
|
||||||
|
v_today_start := date_trunc('day', NOW() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC';
|
||||||
|
v_tomorrow_start := v_today_start + INTERVAL '1 day';
|
||||||
|
|
||||||
|
-- Count responses created today (UTC)
|
||||||
|
SELECT COUNT(*)::INTEGER INTO v_daily_count
|
||||||
|
FROM responses r
|
||||||
|
WHERE r.user_id = v_user_id
|
||||||
|
AND r.created_at >= v_today_start
|
||||||
|
AND r.created_at < v_tomorrow_start;
|
||||||
|
|
||||||
|
-- If over daily limit, return empty (old apps see "All caught up!")
|
||||||
|
IF v_daily_count >= v_daily_limit THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Check answer gap threshold using NET gap (only when not browsing a specific pack)
|
||||||
|
IF v_partner_id IS NOT NULL AND target_pack_id IS NULL THEN
|
||||||
|
-- Get threshold from app_config
|
||||||
|
SELECT COALESCE(ac.answer_gap_threshold, 10) INTO v_gap_threshold
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If threshold > 0, check the NET gap
|
||||||
|
IF v_gap_threshold > 0 THEN
|
||||||
|
-- Calculate NET gap to prevent deadlocks
|
||||||
|
WITH enabled_packs AS (
|
||||||
|
SELECT cp.pack_id
|
||||||
|
FROM couple_packs cp
|
||||||
|
WHERE cp.couple_id = v_couple_id AND cp.enabled = true
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
-- Questions I answered that partner hasn't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_user_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_partner_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer,
|
||||||
|
-- Questions partner answered that I haven't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_partner_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_user_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer
|
||||||
|
INTO v_my_unanswered, v_partner_unanswered
|
||||||
|
FROM responses r
|
||||||
|
INNER JOIN questions q ON q.id = r.question_id
|
||||||
|
WHERE r.couple_id = v_couple_id
|
||||||
|
AND r.user_id IN (v_user_id, v_partner_id)
|
||||||
|
AND EXISTS (SELECT 1 FROM enabled_packs)
|
||||||
|
AND q.pack_id IN (SELECT ep.pack_id FROM enabled_packs ep);
|
||||||
|
|
||||||
|
-- Net gap: only positive values mean user is truly ahead
|
||||||
|
v_net_gap := GREATEST(0, v_my_unanswered - v_partner_unanswered);
|
||||||
|
|
||||||
|
-- If over threshold, return empty (old apps see "All caught up!")
|
||||||
|
IF v_net_gap >= v_gap_threshold THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_user_gender IS NOT NULL AND v_partner_gender IS NOT NULL THEN
|
||||||
|
IF v_user_gender < v_partner_gender THEN
|
||||||
|
v_couple_type := v_user_gender || '+' || v_partner_gender;
|
||||||
|
ELSE
|
||||||
|
v_couple_type := v_partner_gender || '+' || v_user_gender;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
WITH user_config_exists AS (
|
||||||
|
SELECT 1 FROM public.couple_packs WHERE couple_id = v_couple_id LIMIT 1
|
||||||
|
),
|
||||||
|
active_packs AS (
|
||||||
|
SELECT target_pack_id AS pack_id
|
||||||
|
WHERE target_pack_id IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.question_packs qp
|
||||||
|
WHERE qp.id = target_pack_id
|
||||||
|
AND (qp.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT cp.pack_id FROM public.couple_packs cp
|
||||||
|
INNER JOIN public.question_packs qp ON qp.id = cp.pack_id
|
||||||
|
WHERE target_pack_id IS NULL
|
||||||
|
AND cp.couple_id = v_couple_id
|
||||||
|
AND cp.enabled = TRUE
|
||||||
|
AND (qp.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
AND EXISTS (SELECT 1 FROM user_config_exists)
|
||||||
|
UNION ALL
|
||||||
|
SELECT p.id AS pack_id FROM public.question_packs p
|
||||||
|
WHERE target_pack_id IS NULL
|
||||||
|
AND p.is_public = TRUE
|
||||||
|
AND (p.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM user_config_exists)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
q.id,
|
||||||
|
CASE
|
||||||
|
WHEN q.partner_text IS NOT NULL AND EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) THEN q.partner_text
|
||||||
|
ELSE q.text
|
||||||
|
END as text,
|
||||||
|
q.partner_text,
|
||||||
|
q.partner_text IS NOT NULL as is_two_part,
|
||||||
|
q.pack_id,
|
||||||
|
q.intensity,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) AS partner_answered,
|
||||||
|
q.allowed_couple_genders,
|
||||||
|
q.target_user_genders
|
||||||
|
FROM public.questions q
|
||||||
|
WHERE q.pack_id IN (SELECT ap.pack_id FROM active_packs ap)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.user_id = v_user_id
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
q.allowed_couple_genders IS NULL
|
||||||
|
OR v_couple_type IS NULL
|
||||||
|
OR v_couple_type = ANY(q.allowed_couple_genders)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
q.target_user_genders IS NULL
|
||||||
|
OR v_user_gender = ANY(q.target_user_genders)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
-- Filter by user's max_intensity preference
|
||||||
|
AND (
|
||||||
|
q.intensity IS NULL
|
||||||
|
OR q.intensity <= v_user_max_intensity
|
||||||
|
)
|
||||||
|
-- ORDER BY: Partner-answered questions first to help resolve gaps, then by intensity
|
||||||
|
ORDER BY
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) DESC,
|
||||||
|
q.intensity ASC;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION get_recommended_questions(UUID) IS 'Returns recommended questions prioritizing partner-answered questions first to prevent deadlocks. Uses NET gap calculation for blocking.';
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
-- Randomize question order for variety
|
||||||
|
-- Changes ORDER BY from intensity ASC to RANDOM()
|
||||||
|
-- Keeps partner-answered priority for gap resolution
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION get_recommended_questions(target_pack_id UUID DEFAULT NULL)
|
||||||
|
RETURNS TABLE(
|
||||||
|
id UUID,
|
||||||
|
text TEXT,
|
||||||
|
partner_text TEXT,
|
||||||
|
is_two_part BOOLEAN,
|
||||||
|
pack_id UUID,
|
||||||
|
intensity INTEGER,
|
||||||
|
partner_answered BOOLEAN,
|
||||||
|
allowed_couple_genders TEXT[],
|
||||||
|
target_user_genders TEXT[]
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
v_couple_id UUID;
|
||||||
|
v_partner_id UUID;
|
||||||
|
v_user_gender TEXT;
|
||||||
|
v_partner_gender TEXT;
|
||||||
|
v_couple_type TEXT;
|
||||||
|
v_has_premium BOOLEAN;
|
||||||
|
v_gap_threshold INTEGER;
|
||||||
|
v_my_unanswered INTEGER;
|
||||||
|
v_partner_unanswered INTEGER;
|
||||||
|
v_net_gap INTEGER;
|
||||||
|
v_user_max_intensity INTEGER;
|
||||||
|
v_daily_limit INTEGER;
|
||||||
|
v_daily_count INTEGER;
|
||||||
|
v_today_start TIMESTAMPTZ;
|
||||||
|
v_tomorrow_start TIMESTAMPTZ;
|
||||||
|
BEGIN
|
||||||
|
v_user_id := auth.uid();
|
||||||
|
|
||||||
|
SELECT p.couple_id, p.gender, COALESCE(p.max_intensity, 5)
|
||||||
|
INTO v_couple_id, v_user_gender, v_user_max_intensity
|
||||||
|
FROM public.profiles p
|
||||||
|
WHERE p.id = v_user_id;
|
||||||
|
|
||||||
|
IF v_couple_id IS NULL THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT p.id, p.gender INTO v_partner_id, v_partner_gender
|
||||||
|
FROM public.profiles p
|
||||||
|
WHERE p.couple_id = v_couple_id AND p.id != v_user_id
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- Check premium access (used for both answer gap and daily limit)
|
||||||
|
v_has_premium := public.has_premium_access(v_user_id);
|
||||||
|
|
||||||
|
-- Check daily response limit (only when not browsing a specific pack and user is non-premium)
|
||||||
|
IF v_partner_id IS NOT NULL AND target_pack_id IS NULL AND NOT v_has_premium THEN
|
||||||
|
-- Get daily limit from app_config
|
||||||
|
SELECT COALESCE(ac.daily_response_limit, 0) INTO v_daily_limit
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If daily limit > 0, check if user has reached it
|
||||||
|
IF v_daily_limit > 0 THEN
|
||||||
|
-- Calculate UTC day boundaries
|
||||||
|
v_today_start := date_trunc('day', NOW() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC';
|
||||||
|
v_tomorrow_start := v_today_start + INTERVAL '1 day';
|
||||||
|
|
||||||
|
-- Count responses created today (UTC)
|
||||||
|
SELECT COUNT(*)::INTEGER INTO v_daily_count
|
||||||
|
FROM responses r
|
||||||
|
WHERE r.user_id = v_user_id
|
||||||
|
AND r.created_at >= v_today_start
|
||||||
|
AND r.created_at < v_tomorrow_start;
|
||||||
|
|
||||||
|
-- If over daily limit, return empty (old apps see "All caught up!")
|
||||||
|
IF v_daily_count >= v_daily_limit THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Check answer gap threshold using NET gap (only when not browsing a specific pack)
|
||||||
|
IF v_partner_id IS NOT NULL AND target_pack_id IS NULL THEN
|
||||||
|
-- Get threshold from app_config
|
||||||
|
SELECT COALESCE(ac.answer_gap_threshold, 10) INTO v_gap_threshold
|
||||||
|
FROM app_config ac
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If threshold > 0, check the NET gap
|
||||||
|
IF v_gap_threshold > 0 THEN
|
||||||
|
-- Calculate NET gap to prevent deadlocks
|
||||||
|
WITH enabled_packs AS (
|
||||||
|
SELECT cp.pack_id
|
||||||
|
FROM couple_packs cp
|
||||||
|
WHERE cp.couple_id = v_couple_id AND cp.enabled = true
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
-- Questions I answered that partner hasn't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_user_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_partner_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer,
|
||||||
|
-- Questions partner answered that I haven't
|
||||||
|
COALESCE(SUM(CASE WHEN r.user_id = v_partner_id AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM responses pr
|
||||||
|
WHERE pr.question_id = r.question_id
|
||||||
|
AND pr.user_id = v_user_id
|
||||||
|
) THEN 1 ELSE 0 END), 0)::integer
|
||||||
|
INTO v_my_unanswered, v_partner_unanswered
|
||||||
|
FROM responses r
|
||||||
|
INNER JOIN questions q ON q.id = r.question_id
|
||||||
|
WHERE r.couple_id = v_couple_id
|
||||||
|
AND r.user_id IN (v_user_id, v_partner_id)
|
||||||
|
AND EXISTS (SELECT 1 FROM enabled_packs)
|
||||||
|
AND q.pack_id IN (SELECT ep.pack_id FROM enabled_packs ep);
|
||||||
|
|
||||||
|
-- Net gap: only positive values mean user is truly ahead
|
||||||
|
v_net_gap := GREATEST(0, v_my_unanswered - v_partner_unanswered);
|
||||||
|
|
||||||
|
-- If over threshold, return empty (old apps see "All caught up!")
|
||||||
|
IF v_net_gap >= v_gap_threshold THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_user_gender IS NOT NULL AND v_partner_gender IS NOT NULL THEN
|
||||||
|
IF v_user_gender < v_partner_gender THEN
|
||||||
|
v_couple_type := v_user_gender || '+' || v_partner_gender;
|
||||||
|
ELSE
|
||||||
|
v_couple_type := v_partner_gender || '+' || v_user_gender;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
WITH user_config_exists AS (
|
||||||
|
SELECT 1 FROM public.couple_packs WHERE couple_id = v_couple_id LIMIT 1
|
||||||
|
),
|
||||||
|
active_packs AS (
|
||||||
|
SELECT target_pack_id AS pack_id
|
||||||
|
WHERE target_pack_id IS NOT NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM public.question_packs qp
|
||||||
|
WHERE qp.id = target_pack_id
|
||||||
|
AND (qp.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
SELECT cp.pack_id FROM public.couple_packs cp
|
||||||
|
INNER JOIN public.question_packs qp ON qp.id = cp.pack_id
|
||||||
|
WHERE target_pack_id IS NULL
|
||||||
|
AND cp.couple_id = v_couple_id
|
||||||
|
AND cp.enabled = TRUE
|
||||||
|
AND (qp.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
AND EXISTS (SELECT 1 FROM user_config_exists)
|
||||||
|
UNION ALL
|
||||||
|
SELECT p.id AS pack_id FROM public.question_packs p
|
||||||
|
WHERE target_pack_id IS NULL
|
||||||
|
AND p.is_public = TRUE
|
||||||
|
AND (p.is_premium = FALSE OR v_has_premium = TRUE)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM user_config_exists)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
q.id,
|
||||||
|
CASE
|
||||||
|
WHEN q.partner_text IS NOT NULL AND EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) THEN q.partner_text
|
||||||
|
ELSE q.text
|
||||||
|
END as text,
|
||||||
|
q.partner_text,
|
||||||
|
q.partner_text IS NOT NULL as is_two_part,
|
||||||
|
q.pack_id,
|
||||||
|
q.intensity,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) AS partner_answered,
|
||||||
|
q.allowed_couple_genders,
|
||||||
|
q.target_user_genders
|
||||||
|
FROM public.questions q
|
||||||
|
WHERE q.pack_id IN (SELECT ap.pack_id FROM active_packs ap)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.user_id = v_user_id
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
q.allowed_couple_genders IS NULL
|
||||||
|
OR v_couple_type IS NULL
|
||||||
|
OR v_couple_type = ANY(q.allowed_couple_genders)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
q.target_user_genders IS NULL
|
||||||
|
OR v_user_gender = ANY(q.target_user_genders)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
-- Filter by user's max_intensity preference
|
||||||
|
AND (
|
||||||
|
q.intensity IS NULL
|
||||||
|
OR q.intensity <= v_user_max_intensity
|
||||||
|
)
|
||||||
|
-- ORDER BY: Partner-answered questions first, then RANDOM for variety
|
||||||
|
ORDER BY
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM public.responses r
|
||||||
|
WHERE r.question_id = q.id
|
||||||
|
AND r.couple_id = v_couple_id
|
||||||
|
AND r.user_id != v_user_id
|
||||||
|
) DESC,
|
||||||
|
RANDOM();
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION get_recommended_questions(UUID) IS 'Returns recommended questions with partner-answered first, then randomized for variety. Uses NET gap calculation for blocking.';
|
||||||
Reference in new issue
Block a user