sojorn/go-backend/internal/database/migrations/000001_initial_schema.up.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

255 lines
9 KiB
SQL

-- 000001_initial_schema.up.sql
-- EXTENSIONS
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
CREATE EXTENSION IF NOT EXISTS "postgis";
-- TYPES
DO $$ BEGIN
CREATE TYPE beacon_type AS ENUM ('police', 'checkpoint', 'taskForce', 'hazard', 'safety', 'community');
CREATE TYPE trust_tier AS ENUM ('new', 'trusted', 'established');
CREATE TYPE notification_type AS ENUM ('appreciate', 'chain', 'follow', 'comment', 'mention', 'follow_request', 'new_follower', 'request_accepted');
CREATE TYPE tone_label AS ENUM ('positive', 'neutral', 'mixed', 'negative', 'hostile');
CREATE TYPE post_status AS ENUM ('active', 'flagged', 'removed');
EXCEPTION WHEN duplicate_object THEN null; END $$;
-- AUTH SCHEMA (Full Parity)
CREATE SCHEMA IF NOT EXISTS auth;
CREATE TABLE IF NOT EXISTS auth.users (
instance_id UUID,
id UUID PRIMARY KEY,
aud TEXT,
role TEXT,
email TEXT,
encrypted_password TEXT,
email_confirmed_at TIMESTAMPTZ,
invited_at TIMESTAMPTZ,
confirmation_token TEXT,
confirmation_sent_at TIMESTAMPTZ,
recovery_token TEXT,
recovery_sent_at TIMESTAMPTZ,
email_change_token_new TEXT,
email_change TEXT,
email_change_sent_at TIMESTAMPTZ,
last_sign_in_at TIMESTAMPTZ,
raw_app_meta_data JSONB,
raw_user_meta_data JSONB,
is_super_admin BOOLEAN,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ,
phone TEXT DEFAULT NULL,
phone_confirmed_at TIMESTAMPTZ DEFAULT NULL,
phone_change TEXT DEFAULT NULL,
phone_change_sent_at TIMESTAMPTZ DEFAULT NULL,
phone_change_token TEXT DEFAULT NULL,
email_change_token_current TEXT DEFAULT NULL,
email_change_confirm_status SMALLINT DEFAULT 0,
banned_until TIMESTAMPTZ DEFAULT NULL,
reauthentication_token TEXT DEFAULT NULL,
reauthentication_sent_at TIMESTAMPTZ DEFAULT NULL,
is_sso_user BOOLEAN DEFAULT FALSE,
deleted_at TIMESTAMPTZ DEFAULT NULL,
is_anonymous BOOLEAN DEFAULT FALSE
);
-- SUPABASE FUNCTIONS (Compatibility)
CREATE SCHEMA IF NOT EXISTS supabase_functions;
CREATE TABLE IF NOT EXISTS supabase_functions.hooks (
id BIGSERIAL PRIMARY KEY,
hook_table_id INTEGER,
hook_name TEXT,
created_at TIMESTAMPTZ,
request_id TEXT
);
-- CORE TABLES
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT UNIQUE NOT NULL,
password_hash TEXT,
supabase_id UUID UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
is_anonymous BOOLEAN DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
handle TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
bio TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_official BOOLEAN DEFAULT FALSE,
location TEXT,
website TEXT,
interests TEXT[],
avatar_url TEXT,
cover_url TEXT,
account_status TEXT DEFAULT 'active',
deactivated_at TIMESTAMPTZ,
deletion_requested_at TIMESTAMPTZ,
last_handle_change_at TIMESTAMPTZ,
beacon_enabled BOOLEAN NOT NULL DEFAULT FALSE,
strikes INTEGER NOT NULL DEFAULT 0,
role TEXT NOT NULL DEFAULT 'user',
origin_country TEXT,
is_private BOOLEAN NOT NULL DEFAULT FALSE,
identity_key TEXT,
registration_id INTEGER,
encrypted_private_key TEXT
);
CREATE TABLE IF NOT EXISTS categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
slug TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
description TEXT,
is_sensitive BOOLEAN NOT NULL DEFAULT FALSE,
default_off BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS trust_state (
user_id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
harmony_score INTEGER NOT NULL DEFAULT 50,
tier TEXT NOT NULL DEFAULT 'new',
counters JSONB NOT NULL DEFAULT '{}'::jsonb,
last_post_at TIMESTAMPTZ,
posts_today INTEGER NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
tier_changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
author_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
category_id UUID REFERENCES categories(id) ON DELETE SET NULL,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
tone_label TEXT,
cis_score NUMERIC,
status TEXT NOT NULL DEFAULT 'active',
edited_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
allow_chain BOOLEAN NOT NULL DEFAULT TRUE,
chain_parent_id UUID REFERENCES posts(id) ON DELETE SET NULL,
image_url TEXT,
body_format TEXT,
background_id TEXT,
is_edited BOOLEAN NOT NULL DEFAULT FALSE,
tags TEXT[],
location geography(POINT),
is_beacon BOOLEAN DEFAULT FALSE,
beacon_type TEXT,
confidence_score DOUBLE PRECISION,
is_active_beacon BOOLEAN DEFAULT TRUE,
fts TSVECTOR,
expires_at TIMESTAMPTZ,
moderation_status TEXT NOT NULL DEFAULT 'approved',
visibility TEXT NOT NULL DEFAULT 'authenticated',
pinned_at TIMESTAMPTZ,
type TEXT NOT NULL DEFAULT 'post',
video_url TEXT,
thumbnail_url TEXT,
duration_ms INTEGER
);
CREATE TABLE IF NOT EXISTS post_metrics (
post_id UUID PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE,
like_count INTEGER NOT NULL DEFAULT 0,
save_count INTEGER NOT NULL DEFAULT 0,
view_count INTEGER NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS post_likes (
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, post_id)
);
CREATE TABLE IF NOT EXISTS post_saves (
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, post_id)
);
CREATE TABLE IF NOT EXISTS comments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
author_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
tone_label TEXT,
status TEXT NOT NULL DEFAULT 'active',
deleted_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS beacon_votes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
beacon_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
vote_type TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS follows (
follower_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
following_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status TEXT NOT NULL DEFAULT 'accepted',
PRIMARY KEY (follower_id, following_id)
);
CREATE TABLE IF NOT EXISTS notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
type TEXT NOT NULL,
actor_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
post_id UUID REFERENCES posts(id) ON DELETE SET NULL,
comment_id UUID REFERENCES comments(id) ON DELETE SET NULL,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
archived_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS user_fcm_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
token TEXT NOT NULL,
device_type TEXT,
last_updated TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, token)
);
CREATE TABLE IF NOT EXISTS profile_privacy_settings (
user_id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE,
show_location BOOLEAN DEFAULT TRUE,
show_interests BOOLEAN DEFAULT TRUE,
profile_visibility TEXT DEFAULT 'public',
posts_visibility TEXT DEFAULT 'public',
saved_visibility TEXT DEFAULT 'public',
follow_request_policy TEXT DEFAULT 'anyone',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS sponsored_posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
advertiser_name TEXT, -- Added
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_profiles_handle ON profiles(handle);
CREATE INDEX IF NOT EXISTS idx_posts_author ON posts(author_id);
CREATE INDEX IF NOT EXISTS idx_posts_category ON posts(category_id);
CREATE INDEX IF NOT EXISTS idx_posts_location ON posts USING GIST (location);