**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.
216 lines
7.2 KiB
TypeScript
216 lines
7.2 KiB
TypeScript
/**
|
|
* Harmony Score Calculation (Cron Job)
|
|
*
|
|
* This function runs periodically (e.g., daily) to recalculate harmony scores
|
|
* for all users based on their recent behavior patterns.
|
|
*
|
|
* Design intent:
|
|
* - Influence adapts automatically based on behavior.
|
|
* - Scores decay over time (old issues fade).
|
|
* - Changes are gradual, not sudden.
|
|
*
|
|
* Trigger: Scheduled via Supabase cron or external scheduler
|
|
*/
|
|
|
|
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
|
|
import { createServiceClient } from '../_shared/supabase-client.ts';
|
|
import { calculateHarmonyAdjustment, type HarmonyInputs } from '../_shared/harmony.ts';
|
|
|
|
serve(async (req) => {
|
|
try {
|
|
// Verify this is a scheduled/cron request
|
|
const authHeader = req.headers.get('Authorization');
|
|
const cronSecret = Deno.env.get('CRON_SECRET');
|
|
|
|
if (!authHeader || authHeader !== `Bearer ${cronSecret}`) {
|
|
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const serviceClient = createServiceClient();
|
|
|
|
// 1. Get all users with their current trust state
|
|
const { data: users, error: usersError } = await serviceClient
|
|
.from('trust_state')
|
|
.select('user_id, harmony_score, tier, counters')
|
|
.order('user_id');
|
|
|
|
if (usersError) {
|
|
console.error('Error fetching users:', usersError);
|
|
return new Response(JSON.stringify({ error: 'Failed to fetch users' }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
let updatedCount = 0;
|
|
let errorCount = 0;
|
|
|
|
// 2. Process each user
|
|
for (const user of users) {
|
|
try {
|
|
// Gather behavior metrics for this user
|
|
|
|
// Blocks received
|
|
const { count: blocks7d } = await serviceClient
|
|
.from('blocks')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('blocked_id', user.user_id)
|
|
.gte('created_at', sevenDaysAgo);
|
|
|
|
const { count: blocks30d } = await serviceClient
|
|
.from('blocks')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('blocked_id', user.user_id)
|
|
.gte('created_at', thirtyDaysAgo);
|
|
|
|
// Reports against this user
|
|
const { data: reportsAgainst } = await serviceClient
|
|
.from('reports')
|
|
.select('reporter_id, status')
|
|
.or('target_type.eq.post,target_type.eq.comment')
|
|
.in(
|
|
'target_id',
|
|
serviceClient
|
|
.from('posts')
|
|
.select('id')
|
|
.eq('author_id', user.user_id)
|
|
.then((r) => r.data?.map((p) => p.id) || [])
|
|
);
|
|
|
|
// Count trusted reports (from high-harmony reporters)
|
|
let trustedReportsCount = 0;
|
|
if (reportsAgainst) {
|
|
for (const report of reportsAgainst) {
|
|
const { data: reporterTrust } = await serviceClient
|
|
.from('trust_state')
|
|
.select('harmony_score')
|
|
.eq('user_id', report.reporter_id)
|
|
.single();
|
|
|
|
if (reporterTrust && reporterTrust.harmony_score >= 70) {
|
|
trustedReportsCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Posts rejected in last 7 days (from audit log)
|
|
const { data: rejectedPosts } = await serviceClient
|
|
.from('audit_log')
|
|
.select('id')
|
|
.eq('actor_id', user.user_id)
|
|
.eq('event_type', 'post_rejected')
|
|
.gte('created_at', sevenDaysAgo);
|
|
|
|
// Posts created in last 7 days
|
|
const { count: postsCreated7d } = await serviceClient
|
|
.from('posts')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('author_id', user.user_id)
|
|
.gte('created_at', sevenDaysAgo);
|
|
|
|
// Reports filed by user
|
|
const { data: reportsFiled } = await serviceClient
|
|
.from('reports')
|
|
.select('id, status')
|
|
.eq('reporter_id', user.user_id);
|
|
|
|
const falseReports = reportsFiled?.filter((r) => r.status === 'dismissed').length || 0;
|
|
const validatedReports = reportsFiled?.filter((r) => r.status === 'resolved').length || 0;
|
|
|
|
// Days since signup
|
|
const { data: profile } = await serviceClient
|
|
.from('profiles')
|
|
.select('created_at')
|
|
.eq('id', user.user_id)
|
|
.single();
|
|
|
|
const daysSinceSignup = profile
|
|
? Math.floor((Date.now() - new Date(profile.created_at).getTime()) / (1000 * 60 * 60 * 24))
|
|
: 0;
|
|
|
|
// 3. Calculate harmony adjustment
|
|
const inputs: HarmonyInputs = {
|
|
user_id: user.user_id,
|
|
blocks_received_7d: blocks7d || 0,
|
|
blocks_received_30d: blocks30d || 0,
|
|
trusted_reports_against: trustedReportsCount,
|
|
total_reports_against: reportsAgainst?.length || 0,
|
|
posts_rejected_7d: rejectedPosts?.length || 0,
|
|
posts_created_7d: postsCreated7d || 0,
|
|
false_reports_filed: falseReports,
|
|
validated_reports_filed: validatedReports,
|
|
days_since_signup: daysSinceSignup,
|
|
current_harmony_score: user.harmony_score,
|
|
current_tier: user.tier,
|
|
};
|
|
|
|
const adjustment = calculateHarmonyAdjustment(inputs);
|
|
|
|
// 4. Update trust state if score changed
|
|
if (adjustment.delta !== 0) {
|
|
const { error: updateError } = await serviceClient
|
|
.from('trust_state')
|
|
.update({
|
|
harmony_score: adjustment.new_score,
|
|
tier: adjustment.new_tier,
|
|
updated_at: new Date().toISOString(),
|
|
})
|
|
.eq('user_id', user.user_id);
|
|
|
|
if (updateError) {
|
|
console.error(`Error updating trust state for ${user.user_id}:`, updateError);
|
|
errorCount++;
|
|
continue;
|
|
}
|
|
|
|
// 5. Log the adjustment
|
|
await serviceClient.rpc('log_audit_event', {
|
|
p_actor_id: null, // system action
|
|
p_event_type: 'harmony_recalculated',
|
|
p_payload: {
|
|
user_id: user.user_id,
|
|
old_score: user.harmony_score,
|
|
new_score: adjustment.new_score,
|
|
delta: adjustment.delta,
|
|
old_tier: user.tier,
|
|
new_tier: adjustment.new_tier,
|
|
reason: adjustment.reason,
|
|
},
|
|
});
|
|
|
|
updatedCount++;
|
|
}
|
|
} catch (userError) {
|
|
console.error(`Error processing user ${user.user_id}:`, userError);
|
|
errorCount++;
|
|
}
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
total_users: users.length,
|
|
updated: updatedCount,
|
|
errors: errorCount,
|
|
message: 'Harmony score recalculation complete',
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}
|
|
);
|
|
} catch (error) {
|
|
console.error('Unexpected error:', error);
|
|
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
});
|