sojorn/_legacy/supabase/migrations/20260119_e2ee_session_state.sql
Patrick Britton 3c4680bdd7 Initial commit: Complete threaded conversation system with inline replies
**Major Features Added:**
- **Inline Reply System**: Replace compose screen with inline reply boxes
- **Thread Navigation**: Parent/child navigation with jump functionality
- **Chain Flow UI**: Reply counts, expand/collapse animations, visual hierarchy
- **Enhanced Animations**: Smooth transitions, hover effects, micro-interactions

 **Frontend Changes:**
- **ThreadedCommentWidget**: Complete rewrite with animations and navigation
- **ThreadNode Model**: Added parent references and descendant counting
- **ThreadedConversationScreen**: Integrated navigation handlers
- **PostDetailScreen**: Replaced with threaded conversation view
- **ComposeScreen**: Added reply indicators and context
- **PostActions**: Fixed visibility checks for chain buttons

 **Backend Changes:**
- **API Route**: Added /posts/:id/thread endpoint
- **Post Repository**: Include allow_chain and visibility fields in feed
- **Thread Handler**: Support for fetching post chains

 **UI/UX Improvements:**
- **Reply Context**: Clear indication when replying to specific posts
- **Character Counting**: 500 character limit with live counter
- **Visual Hierarchy**: Depth-based indentation and styling
- **Smooth Animations**: SizeTransition, FadeTransition, hover states
- **Chain Navigation**: Parent/child buttons with visual feedback

 **Technical Enhancements:**
- **Animation Controllers**: Proper lifecycle management
- **State Management**: Clean separation of concerns
- **Navigation Callbacks**: Reusable navigation system
- **Error Handling**: Graceful fallbacks and user feedback

This creates a Reddit-style threaded conversation experience with smooth
animations, inline replies, and intuitive navigation between posts in a chain.
2026-01-30 07:40:19 -06:00

256 lines
11 KiB
PL/PgSQL

-- ============================================================================
-- E2EE Session State Tracking
-- ============================================================================
-- Server-side session state tracking to detect and recover from session
-- mismatches between parties. The server cannot see session keys, only
-- metadata about whether sessions exist.
-- ============================================================================
-- ============================================================================
-- 1. Session State Table
-- ============================================================================
-- Track which users have established sessions with each other
-- This allows detection of asymmetric session states (one party has session, other doesn't)
CREATE TABLE IF NOT EXISTS e2ee_session_state (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Session participants (always stored with user_id < peer_id for consistency)
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
peer_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Session state flags
user_has_session BOOLEAN NOT NULL DEFAULT FALSE,
peer_has_session BOOLEAN NOT NULL DEFAULT FALSE,
-- Session metadata (no actual keys stored!)
user_session_created_at TIMESTAMPTZ,
peer_session_created_at TIMESTAMPTZ,
-- Version tracking for conflict resolution
user_session_version INTEGER NOT NULL DEFAULT 0,
peer_session_version INTEGER NOT NULL DEFAULT 0,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Ensure unique pair (user_id should always be < peer_id)
CONSTRAINT e2ee_session_state_pair_unique UNIQUE (user_id, peer_id),
CONSTRAINT e2ee_session_state_ordering CHECK (user_id < peer_id)
);
-- Indexes for efficient lookups
CREATE INDEX IF NOT EXISTS idx_session_state_user ON e2ee_session_state(user_id);
CREATE INDEX IF NOT EXISTS idx_session_state_peer ON e2ee_session_state(peer_id);
CREATE INDEX IF NOT EXISTS idx_session_state_mismatch ON e2ee_session_state(user_has_session, peer_has_session)
WHERE user_has_session != peer_has_session;
-- ============================================================================
-- 2. RLS Policies
-- ============================================================================
ALTER TABLE e2ee_session_state ENABLE ROW LEVEL SECURITY;
-- Users can see session state for their own sessions
CREATE POLICY session_state_select_own ON e2ee_session_state
FOR SELECT USING (auth.uid() = user_id OR auth.uid() = peer_id);
-- Users can only insert/update their own side of the session
-- (handled via function to enforce ordering)
-- ============================================================================
-- 3. Helper Functions
-- ============================================================================
-- Function to update session state (handles ordering automatically)
CREATE OR REPLACE FUNCTION update_e2ee_session_state(
p_user_id UUID,
p_peer_id UUID,
p_has_session BOOLEAN
) RETURNS JSONB AS $$
DECLARE
v_lower_id UUID;
v_higher_id UUID;
v_is_user_lower BOOLEAN;
v_result JSONB;
v_session_state RECORD;
BEGIN
-- Determine ordering (user_id < peer_id constraint)
IF p_user_id < p_peer_id THEN
v_lower_id := p_user_id;
v_higher_id := p_peer_id;
v_is_user_lower := TRUE;
ELSE
v_lower_id := p_peer_id;
v_higher_id := p_user_id;
v_is_user_lower := FALSE;
END IF;
-- Insert or update
INSERT INTO e2ee_session_state (user_id, peer_id, user_has_session, peer_has_session, user_session_created_at, peer_session_created_at, user_session_version, peer_session_version)
VALUES (
v_lower_id,
v_higher_id,
CASE WHEN v_is_user_lower THEN p_has_session ELSE FALSE END,
CASE WHEN NOT v_is_user_lower THEN p_has_session ELSE FALSE END,
CASE WHEN v_is_user_lower AND p_has_session THEN NOW() ELSE NULL END,
CASE WHEN NOT v_is_user_lower AND p_has_session THEN NOW() ELSE NULL END,
CASE WHEN v_is_user_lower THEN 1 ELSE 0 END,
CASE WHEN NOT v_is_user_lower THEN 1 ELSE 0 END
)
ON CONFLICT (user_id, peer_id) DO UPDATE SET
user_has_session = CASE
WHEN v_is_user_lower THEN p_has_session
ELSE e2ee_session_state.user_has_session
END,
peer_has_session = CASE
WHEN NOT v_is_user_lower THEN p_has_session
ELSE e2ee_session_state.peer_has_session
END,
user_session_created_at = CASE
WHEN v_is_user_lower AND p_has_session AND e2ee_session_state.user_session_created_at IS NULL THEN NOW()
WHEN v_is_user_lower AND NOT p_has_session THEN NULL
ELSE e2ee_session_state.user_session_created_at
END,
peer_session_created_at = CASE
WHEN NOT v_is_user_lower AND p_has_session AND e2ee_session_state.peer_session_created_at IS NULL THEN NOW()
WHEN NOT v_is_user_lower AND NOT p_has_session THEN NULL
ELSE e2ee_session_state.peer_session_created_at
END,
user_session_version = CASE
WHEN v_is_user_lower THEN e2ee_session_state.user_session_version + 1
ELSE e2ee_session_state.user_session_version
END,
peer_session_version = CASE
WHEN NOT v_is_user_lower THEN e2ee_session_state.peer_session_version + 1
ELSE e2ee_session_state.peer_session_version
END,
updated_at = NOW()
RETURNING * INTO v_session_state;
-- Build result with mismatch detection
v_result := jsonb_build_object(
'success', TRUE,
'user_has_session', CASE WHEN v_is_user_lower THEN v_session_state.user_has_session ELSE v_session_state.peer_has_session END,
'peer_has_session', CASE WHEN v_is_user_lower THEN v_session_state.peer_has_session ELSE v_session_state.user_has_session END,
'session_mismatch', v_session_state.user_has_session != v_session_state.peer_has_session,
'peer_session_version', CASE WHEN v_is_user_lower THEN v_session_state.peer_session_version ELSE v_session_state.user_session_version END
);
RETURN v_result;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to get session state between two users
CREATE OR REPLACE FUNCTION get_e2ee_session_state(
p_user_id UUID,
p_peer_id UUID
) RETURNS JSONB AS $$
DECLARE
v_lower_id UUID;
v_higher_id UUID;
v_is_user_lower BOOLEAN;
v_session_state RECORD;
BEGIN
-- Determine ordering
IF p_user_id < p_peer_id THEN
v_lower_id := p_user_id;
v_higher_id := p_peer_id;
v_is_user_lower := TRUE;
ELSE
v_lower_id := p_peer_id;
v_higher_id := p_user_id;
v_is_user_lower := FALSE;
END IF;
SELECT * INTO v_session_state
FROM e2ee_session_state
WHERE user_id = v_lower_id AND peer_id = v_higher_id;
IF NOT FOUND THEN
RETURN jsonb_build_object(
'exists', FALSE,
'user_has_session', FALSE,
'peer_has_session', FALSE,
'session_mismatch', FALSE
);
END IF;
RETURN jsonb_build_object(
'exists', TRUE,
'user_has_session', CASE WHEN v_is_user_lower THEN v_session_state.user_has_session ELSE v_session_state.peer_has_session END,
'peer_has_session', CASE WHEN v_is_user_lower THEN v_session_state.peer_has_session ELSE v_session_state.user_has_session END,
'session_mismatch', v_session_state.user_has_session != v_session_state.peer_has_session,
'user_session_version', CASE WHEN v_is_user_lower THEN v_session_state.user_session_version ELSE v_session_state.peer_session_version END,
'peer_session_version', CASE WHEN v_is_user_lower THEN v_session_state.peer_session_version ELSE v_session_state.user_session_version END
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to clear session state (when resetting)
CREATE OR REPLACE FUNCTION clear_e2ee_session_state(
p_user_id UUID,
p_peer_id UUID
) RETURNS JSONB AS $$
DECLARE
v_lower_id UUID;
v_higher_id UUID;
v_is_user_lower BOOLEAN;
BEGIN
-- Determine ordering
IF p_user_id < p_peer_id THEN
v_lower_id := p_user_id;
v_higher_id := p_peer_id;
v_is_user_lower := TRUE;
ELSE
v_lower_id := p_peer_id;
v_higher_id := p_user_id;
v_is_user_lower := FALSE;
END IF;
-- Update only the caller's side of the session
UPDATE e2ee_session_state SET
user_has_session = CASE WHEN v_is_user_lower THEN FALSE ELSE user_has_session END,
peer_has_session = CASE WHEN NOT v_is_user_lower THEN FALSE ELSE peer_has_session END,
user_session_created_at = CASE WHEN v_is_user_lower THEN NULL ELSE user_session_created_at END,
peer_session_created_at = CASE WHEN NOT v_is_user_lower THEN NULL ELSE peer_session_created_at END,
user_session_version = CASE WHEN v_is_user_lower THEN user_session_version + 1 ELSE user_session_version END,
peer_session_version = CASE WHEN NOT v_is_user_lower THEN peer_session_version + 1 ELSE peer_session_version END,
updated_at = NOW()
WHERE user_id = v_lower_id AND peer_id = v_higher_id;
RETURN jsonb_build_object('success', TRUE);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- ============================================================================
-- 4. Add decryption_failure event type if not exists
-- ============================================================================
-- Update the check constraint to include session_mismatch event type
ALTER TABLE e2ee_session_events
DROP CONSTRAINT IF EXISTS e2ee_session_events_event_type_check;
ALTER TABLE e2ee_session_events
ADD CONSTRAINT e2ee_session_events_event_type_check
CHECK (event_type IN ('session_reset', 'conversation_cleanup', 'key_refresh', 'decryption_failure', 'session_mismatch', 'session_established'));
-- ============================================================================
-- 5. Realtime for session state changes
-- ============================================================================
ALTER PUBLICATION supabase_realtime ADD TABLE e2ee_session_state;
-- ============================================================================
-- 6. Comments
-- ============================================================================
COMMENT ON TABLE e2ee_session_state IS 'Server-side tracking of E2EE session existence between user pairs. Does NOT store actual keys.';
COMMENT ON COLUMN e2ee_session_state.user_has_session IS 'Whether the user with smaller UUID has an active session';
COMMENT ON COLUMN e2ee_session_state.peer_has_session IS 'Whether the user with larger UUID has an active session';
COMMENT ON COLUMN e2ee_session_state.user_session_version IS 'Incremented each time user updates their session state';
COMMENT ON FUNCTION update_e2ee_session_state IS 'Update session state for a user-peer pair. Handles UUID ordering automatically.';
COMMENT ON FUNCTION get_e2ee_session_state IS 'Get session state between two users. Returns mismatch detection.';
COMMENT ON FUNCTION clear_e2ee_session_state IS 'Clear session state for a user (used during session reset).';