Fix notifications: add archived_at to model/queries, archived tab returns only archived, add bottom nav to notifications screen

This commit is contained in:
Patrick Britton 2026-02-06 14:30:18 -06:00
parent 46566f394b
commit 8186e9e71c
4 changed files with 332 additions and 325 deletions

View file

@ -33,17 +33,18 @@ const (
) )
type Notification struct { type Notification struct {
ID uuid.UUID `json:"id" db:"id"` ID uuid.UUID `json:"id" db:"id"`
UserID uuid.UUID `json:"user_id" db:"user_id"` UserID uuid.UUID `json:"user_id" db:"user_id"`
Type string `json:"type" db:"type"` Type string `json:"type" db:"type"`
ActorID uuid.UUID `json:"actor_id" db:"actor_id"` ActorID uuid.UUID `json:"actor_id" db:"actor_id"`
PostID *uuid.UUID `json:"post_id,omitempty" db:"post_id"` PostID *uuid.UUID `json:"post_id,omitempty" db:"post_id"`
CommentID *uuid.UUID `json:"comment_id,omitempty" db:"comment_id"` CommentID *uuid.UUID `json:"comment_id,omitempty" db:"comment_id"`
IsRead bool `json:"is_read" db:"is_read"` IsRead bool `json:"is_read" db:"is_read"`
CreatedAt time.Time `json:"created_at" db:"created_at"` CreatedAt time.Time `json:"created_at" db:"created_at"`
Metadata json.RawMessage `json:"metadata" db:"metadata"` ArchivedAt *time.Time `json:"archived_at,omitempty" db:"archived_at"`
GroupKey *string `json:"group_key,omitempty" db:"group_key"` Metadata json.RawMessage `json:"metadata" db:"metadata"`
Priority string `json:"priority" db:"priority"` GroupKey *string `json:"group_key,omitempty" db:"group_key"`
Priority string `json:"priority" db:"priority"`
// Joined fields for display // Joined fields for display
ActorHandle string `json:"actor_handle" db:"actor_handle"` ActorHandle string `json:"actor_handle" db:"actor_handle"`

View file

@ -107,12 +107,12 @@ func (r *NotificationRepository) CreateNotification(ctx context.Context, notif *
func (r *NotificationRepository) GetNotifications(ctx context.Context, userID string, limit, offset int, includeArchived bool) ([]models.Notification, error) { func (r *NotificationRepository) GetNotifications(ctx context.Context, userID string, limit, offset int, includeArchived bool) ([]models.Notification, error) {
whereClause := "WHERE n.user_id = $1::uuid AND n.archived_at IS NULL" whereClause := "WHERE n.user_id = $1::uuid AND n.archived_at IS NULL"
if includeArchived { if includeArchived {
whereClause = "WHERE n.user_id = $1::uuid" whereClause = "WHERE n.user_id = $1::uuid AND n.archived_at IS NOT NULL"
} }
query := ` query := `
SELECT SELECT
n.id, n.user_id, n.type, n.actor_id, n.post_id, n.comment_id, n.is_read, n.created_at, n.metadata, n.id, n.user_id, n.type, n.actor_id, n.post_id, n.comment_id, n.is_read, n.created_at, n.archived_at, n.metadata,
COALESCE(n.group_key, '') as group_key, COALESCE(n.group_key, '') as group_key,
COALESCE(n.priority, 'normal') as priority, COALESCE(n.priority, 'normal') as priority,
pr.handle, pr.display_name, COALESCE(pr.avatar_url, ''), pr.handle, pr.display_name, COALESCE(pr.avatar_url, ''),
@ -138,7 +138,7 @@ func (r *NotificationRepository) GetNotifications(ctx context.Context, userID st
var postImageURL *string var postImageURL *string
var postBody *string var postBody *string
err := rows.Scan( err := rows.Scan(
&n.ID, &n.UserID, &n.Type, &n.ActorID, &n.PostID, &n.CommentID, &n.IsRead, &n.CreatedAt, &n.Metadata, &n.ID, &n.UserID, &n.Type, &n.ActorID, &n.PostID, &n.CommentID, &n.IsRead, &n.CreatedAt, &n.ArchivedAt, &n.Metadata,
&groupKey, &n.Priority, &groupKey, &n.Priority,
&n.ActorHandle, &n.ActorDisplayName, &n.ActorAvatarURL, &n.ActorHandle, &n.ActorDisplayName, &n.ActorAvatarURL,
&postImageURL, &postBody, &postImageURL, &postBody,
@ -160,7 +160,7 @@ func (r *NotificationRepository) GetNotifications(ctx context.Context, userID st
func (r *NotificationRepository) GetGroupedNotifications(ctx context.Context, userID string, limit, offset int, includeArchived bool) ([]models.Notification, error) { func (r *NotificationRepository) GetGroupedNotifications(ctx context.Context, userID string, limit, offset int, includeArchived bool) ([]models.Notification, error) {
whereClause := "WHERE n.user_id = $1::uuid AND n.archived_at IS NULL" whereClause := "WHERE n.user_id = $1::uuid AND n.archived_at IS NULL"
if includeArchived { if includeArchived {
whereClause = "WHERE n.user_id = $1::uuid" whereClause = "WHERE n.user_id = $1::uuid AND n.archived_at IS NOT NULL"
} }
query := ` query := `
@ -180,7 +180,7 @@ func (r *NotificationRepository) GetGroupedNotifications(ctx context.Context, us
` + whereClause + ` ` + whereClause + `
) )
SELECT SELECT
id, user_id, type, actor_id, post_id, comment_id, is_read, created_at, metadata, id, user_id, type, actor_id, post_id, comment_id, is_read, created_at, archived_at, metadata,
COALESCE(group_key, '') as group_key, COALESCE(group_key, '') as group_key,
COALESCE(priority, 'normal') as priority, COALESCE(priority, 'normal') as priority,
actor_handle, actor_display_name, actor_avatar_url, actor_handle, actor_display_name, actor_avatar_url,
@ -203,7 +203,7 @@ func (r *NotificationRepository) GetGroupedNotifications(ctx context.Context, us
var n models.Notification var n models.Notification
var groupKey string var groupKey string
err := rows.Scan( err := rows.Scan(
&n.ID, &n.UserID, &n.Type, &n.ActorID, &n.PostID, &n.CommentID, &n.IsRead, &n.CreatedAt, &n.Metadata, &n.ID, &n.UserID, &n.Type, &n.ActorID, &n.PostID, &n.CommentID, &n.IsRead, &n.CreatedAt, &n.ArchivedAt, &n.Metadata,
&groupKey, &n.Priority, &groupKey, &n.Priority,
&n.ActorHandle, &n.ActorDisplayName, &n.ActorAvatarURL, &n.ActorHandle, &n.ActorDisplayName, &n.ActorAvatarURL,
&n.PostImageURL, &n.PostBody, &n.PostImageURL, &n.PostBody,

View file

@ -10,6 +10,9 @@ import '../../widgets/app_scaffold.dart';
import '../../widgets/media/signed_media_image.dart'; import '../../widgets/media/signed_media_image.dart';
import '../profile/viewable_profile_screen.dart'; import '../profile/viewable_profile_screen.dart';
import '../post/post_detail_screen.dart'; import '../post/post_detail_screen.dart';
import '../search/search_screen.dart';
import '../discover/discover_screen.dart';
import '../secure_chat/secure_chat_full_screen.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../services/notification_service.dart'; import '../../services/notification_service.dart';
@ -51,6 +54,7 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
void dispose() { void dispose() {
_authSub?.cancel(); _authSub?.cancel();
_tabController?.dispose(); _tabController?.dispose();
NotificationService.instance.refreshBadge();
super.dispose(); super.dispose();
} }
@ -108,28 +112,18 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
); );
if (mounted) { if (mounted) {
// If showing Active, filter out anything archived // Backend handles active vs archived filtering
final filtered = !showArchived final filtered = notifications
? notifications .where((item) => !_locallyArchivedIds.contains(item.id))
.where((item) => .toList();
item.archivedAt == null &&
!_locallyArchivedIds.contains(item.id))
.toList()
// If showing Archived, only show archived items
: notifications
.where((item) =>
item.archivedAt != null ||
_locallyArchivedIds.contains(item.id))
.toList();
final fetchedCount = notifications.length;
setState(() { setState(() {
if (refresh) { if (refresh) {
_notifications = filtered; _notifications = filtered;
} else { } else {
_notifications.addAll(filtered); _notifications.addAll(filtered);
} }
_hasMore = fetchedCount == 20; _hasMore = notifications.length == 20;
}); });
} }
} catch (e) { } catch (e) {
@ -358,6 +352,25 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
} }
} }
void _navigateHome() {
Navigator.of(context).pop();
}
void _navigateSearch() {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const DiscoverScreen()),
);
}
void _navigateChat() {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const SecureChatFullScreen(),
fullscreenDialog: true,
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final canArchiveAll = _activeTabIndex == 0 && _notifications.isNotEmpty; final canArchiveAll = _activeTabIndex == 0 && _notifications.isNotEmpty;
@ -365,11 +378,8 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
return DefaultTabController( return DefaultTabController(
length: 2, length: 2,
child: AppScaffold( child: AppScaffold(
title: 'Notifications', title: 'Activity',
leading: IconButton( leading: const SizedBox.shrink(),
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
actions: [ actions: [
if (canArchiveAll) if (canArchiveAll)
TextButton( TextButton(
@ -400,6 +410,7 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
Tab(text: 'Archived'), Tab(text: 'Archived'),
], ],
), ),
bottomNavigationBar: _buildBottomNav(),
body: _error != null body: _error != null
? _ErrorState( ? _ErrorState(
message: _error!, message: _error!,
@ -414,7 +425,6 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == _notifications.length) { if (index == _notifications.length) {
// Load more indicator
if (!_isLoading) { if (!_isLoading) {
_loadNotifications(); _loadNotifications();
} }
@ -428,7 +438,6 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
final notification = _notifications[index]; final notification = _notifications[index];
if (_activeTabIndex == 0) { if (_activeTabIndex == 0) {
// Swipe to archive in Active tab
return Dismissible( return Dismissible(
key: Key('notif_${notification.id}'), key: Key('notif_${notification.id}'),
direction: DismissDirection.endToStart, direction: DismissDirection.endToStart,
@ -466,7 +475,6 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
), ),
); );
} else { } else {
// No swipe to archive in Archived tab
return _NotificationItem( return _NotificationItem(
notification: notification, notification: notification,
onTap: notification.type == NotificationType.follow_request onTap: notification.type == NotificationType.follow_request
@ -480,6 +488,86 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
), ),
); );
} }
Widget _buildBottomNav() {
return Container(
decoration: BoxDecoration(
color: AppTheme.scaffoldBg,
border: Border(
top: BorderSide(
color: AppTheme.egyptianBlue.withOpacity(0.1),
width: 0.5,
),
),
),
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavItem(
icon: Icons.home_outlined,
label: 'Home',
onTap: _navigateHome,
),
_buildNavItem(
icon: Icons.search,
label: 'Discover',
onTap: _navigateSearch,
),
_buildNavItem(
icon: Icons.notifications,
label: 'Activity',
isActive: true,
onTap: () {},
),
_buildNavItem(
icon: Icons.chat_bubble_outline,
label: 'Chat',
onTap: _navigateChat,
),
],
),
),
),
);
}
Widget _buildNavItem({
required IconData icon,
required String label,
required VoidCallback onTap,
bool isActive = false,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: isActive ? AppTheme.navyBlue : Colors.grey,
size: 26,
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 10,
color: isActive ? AppTheme.navyBlue : Colors.grey,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
),
);
}
} }
/// Individual notification item widget /// Individual notification item widget

View file

@ -1,311 +1,229 @@
# Sojorn Development TODO List # Sojorn Development TODO
**Last Updated**: January 30, 2026 **Last Updated**: February 6, 2026
**Status**: Ready for 100% Go Backend Migration
**Estimated Effort**: ~12 hours total
--- ---
## 🎯 **High Priority Tasks** (Critical for 100% Go Backend) ## 🎯 High Priority — Feature Work
### **1. Implement Beacon Voting Endpoints** ### 1. Finalize AI Moderation — Image & Video
**Location**: `go-backend/internal/handlers/post_handler.go` **Status**: In Progress
**Status**: Not Started Text moderation is live (OpenAI Moderation API). Image and video moderation not yet implemented.
**Effort**: 2 hours
**Tasks:** - [ ] Add image moderation to post creation flow (send image to OpenAI or Vision API)
- [ ] Add `VouchBeacon` handler method - [ ] Add video moderation via thumbnail extraction (grab first frame or key frame)
- [ ] Add `ReportBeacon` handler method - [ ] Run extracted thumbnail through same image moderation pipeline
- [ ] Add `RemoveBeaconVote` handler method - [ ] Flag content that exceeds thresholds into `moderation_flags` table
- [ ] Implement database operations for beacon votes - [ ] Wire into existing Three Poisons scoring (Hate, Greed, Delusion)
- [ ] Add proper error handling and validation - [ ] Add admin queue visibility for image/video flags
**Implementation Details:** **Backend**: `go-backend/internal/handlers/post_handler.go` (CreatePost flow)
```go **Key decision**: Use OpenAI Vision API for images, ffmpeg thumbnail extraction for video on the server side
func (h *PostHandler) VouchBeacon(c *gin.Context) {
// Get user ID from context
// Get beacon ID from params
// Check if user already voted
// Add vote to database
// Update beacon confidence score
// Return success response
}
```
### **2. Add Beacon Voting Routes**
**Location**: `go-backend/cmd/api/main.go`
**Status**: Not Started
**Effort**: 30 minutes
**Tasks:**
- [ ] Add `POST /beacons/:id/vouch` route
- [ ] Add `POST /beacons/:id/report` route
- [ ] Add `DELETE /beacons/:id/vouch` route
- [ ] Ensure proper middleware (auth, rate limiting)
**Routes to Add:**
```go
authorized.POST("/beacons/:id/vouch", postHandler.VouchBeacon)
authorized.POST("/beacons/:id/report", postHandler.ReportBeacon)
authorized.DELETE("/beacons/:id/vouch", postHandler.RemoveBeaconVote)
```
### **3. Update Flutter Beacon API**
**Location**: `sojorn_app/lib/services/api_service.dart`
**Status**: Not Started
**Effort**: 1 hour
**Tasks:**
- [ ] Replace empty `vouchBeacon()` method
- [ ] Replace empty `reportBeacon()` method
- [ ] Replace empty `removeBeaconVote()` method
- [ ] Add proper error handling
- [ ] Update method signatures to match Go API
**Current State:**
```dart
// These methods are empty stubs that need implementation:
Future<void> vouchBeacon(String beaconId) async {
// Migrate to Go API - EMPTY STUB
}
Future<void> reportBeacon(String beaconId) async {
// Migrate to Go API - EMPTY STUB
}
Future<void> removeBeaconVote(String beaconId) async {
// Migrate to Go API - EMPTY STUB
}
```
--- ---
## ⚠️ **Medium Priority Tasks** (Cleanup & Technical Debt) ### 2. Quips — Complete Video Recorder & Editor Overhaul
**Status**: Needs major work
Current recorder is basic. Goal: TikTok/Instagram-level recording and editing experience.
### **4. Remove Supabase Function Proxy** - [ ] Multi-segment recording with pause/resume
**Location**: `go-backend/internal/handlers/function_proxy.go` - [ ] Speed control (0.5x, 1x, 2x, 3x)
**Status**: Not Started - [ ] Filters and effects (color grading, beauty mode)
**Effort**: 1 hour - [ ] Text overlays with timing and positioning
- [ ] Music/audio overlay from library or device
- [ ] Trim and reorder clips
- [ ] Transitions between segments
- [ ] Preview before posting
- [ ] Progress indicator during upload
- [ ] Thumbnail selection for posted quip
**Tasks:** **Frontend**: `sojorn_app/lib/screens/quips/create/`
- [ ] Delete `function_proxy.go` file **Packages to evaluate**: `camera`, `ffmpeg_kit_flutter`, `video_editor`
- [ ] Remove function proxy handler instantiation
- [ ] Remove `/functions/:name` route from `main.go`
- [ ] Remove related environment variables
- [ ] Test that no functionality is broken
**Files to Remove:**
- `go-backend/internal/handlers/function_proxy.go`
- `go-backend/cmd/supabase-migrate/` (entire directory)
### **5. Clean Up Supabase Dependencies**
**Location**: Multiple files
**Status**: Not Started
**Effort**: 2 hours
**Tasks:**
- [ ] Remove `SupabaseID` field from `internal/models/user.go`
- [ ] Remove Supabase environment variables from `.env.example`
- [ ] Update configuration struct in `internal/config/config.go`
- [ ] Remove any remaining Supabase imports
- [ ] Update middleware comments that reference Supabase
**Environment Variables to Remove:**
```bash
# SUPABASE_URL
# SUPABASE_KEY
# SUPABASE_SERVICE_ROLE_KEY
```
### **6. Update Outdated TODO Comments**
**Location**: `sojorn_app/lib/services/api_service.dart`
**Status**: Not Started
**Effort**: 30 minutes
**Tasks:**
- [ ] Remove comment "Beacon voting still Supabase RPC or migrate?"
- [ ] Remove comment "Summary didn't mention beacon voting endpoint"
- [ ] Update any other misleading Supabase references
- [ ] Ensure all comments reflect current Go backend state
### **7. End-to-End Testing**
**Location**: Entire application
**Status**: Not Started
**Effort**: 2 hours
**Tasks:**
- [ ] Test beacon voting flow end-to-end
- [ ] Verify all core features work without Supabase
- [ ] Test media uploads to R2
- [ ] Test E2EE chat functionality
- [ ] Test push notifications
- [ ] Performance testing
- [ ] Security verification
--- ---
## 🔧 **Low Priority Tasks** (UI Polish & Code Cleanup) ### 3. Beacon Page Overhaul — Local Safety & Social Awareness
**Status**: Basic beacon system exists (post, vouch, report). Needs full redesign.
Vision: Citizen + Nextdoor but focused on social awareness over fear-mongering.
### **8. Fix Video Comments TODO** - [ ] Redesign beacon feed as a local safety dashboard
**Location**: `sojorn_app/lib/widgets/video_comments_sheet.dart` - [ ] Map view with clustered pins (incidents, community alerts, mutual aid)
**Status**: Not Started - [ ] Beacon categories: Safety Alert, Community Need, Lost & Found, Event, Mutual Aid
**Effort**: 1 hour - [ ] Verified/official source badges for local orgs
- [ ] "How to help" action items on each beacon (donate, volunteer, share)
- [ ] Tone guidelines — auto-moderate fear-bait and rage-bait language
- [ ] Neighborhood/radius filtering
- [ ] Push notifications for nearby beacons (opt-in)
- [ ] Confidence scoring visible to users (vouch/report ratio)
- [ ] Resolution status (active → resolved → archived)
**Tasks:** **Backend**: `go-backend/internal/handlers/post_handler.go` (beacon endpoints)
- [ ] Replace simulated API call with real `publishComment()` call **Frontend**: `sojorn_app/lib/screens/beacons/`
- [ ] Remove "TODO: Implement actual comment posting" comment
- [ ] Test comment posting functionality
- [ ] Ensure proper error handling
**Current State:**
```dart
// TODO: Implement actual comment posting
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
```
**Should Become:**
```dart
await ApiService.instance.publishComment(postId: widget.postId, body: comment);
```
### **9. Implement Comment Reply Features**
**Location**: `sojorn_app/lib/widgets/threaded_comment_widget.dart`
**Status**: Not Started
**Effort**: 2 hours
**Tasks:**
- [ ] Implement actual reply submission in `_submitReply()`
- [ ] Add reply UI components
- [ ] Connect to backend reply API
- [ ] Remove "TODO: Implement actual reply submission" comment
### **10. Add Post Options Menu**
**Location**: `sojorn_app/lib/widgets/post_with_video_widget.dart`
**Status**: Not Started
**Effort**: 1 hour
**Tasks:**
- [ ] Implement post options menu functionality
- [ ] Add menu items (edit, delete, report, etc.)
- [ ] Remove "TODO: Show post options" comment
- [ ] Connect to backend APIs
### **11. Fix Profile Navigation**
**Location**: `sojorn_app/lib/widgets/sojorn_rich_text.dart`
**Status**: Not Started
**Effort**: 1 hour
**Tasks:**
- [ ] Implement profile navigation from mentions
- [ ] Remove "TODO: Implement profile navigation" comment
- [ ] Add proper navigation logic
- [ ] Test mention navigation
### **12. Clean Up Debug Code**
**Location**: `sojorn_app/lib/services/simple_e2ee_service.dart`
**Status**: Not Started
**Effort**: 1 hour
**Tasks:**
- [ ] Remove `_FORCE_KEY_ROTATION` debug flag
- [ ] Remove debug print statements
- [ ] Remove force reset methods for 208-bit key bug
- [ ] Clean up any remaining debug code
--- ---
## **Already Completed Features** (For Reference) ## ⚠️ Medium Priority — Core Features
### **Core Functionality** ### 4. User Profile Customization — Modular Widget System
- ✅ User authentication (JWT-based) **Status**: Basic profiles exist. Needs a modular, personalized approach.
- ✅ Post creation, editing, deletion Vision: New-age MySpace — users pick and arrange profile widgets to make it their own, without chaos.
- ✅ Feed and post retrieval
- ✅ Image/video uploads to R2 **Core Architecture:**
- ✅ **Comment system** (fully implemented) - Profile is a grid/stack of draggable **widgets** the user can add, remove, and reorder
- Each widget is a self-contained component with a fixed max size and style boundary
- Widgets render inside a consistent design system (can't break the layout or go full HTML)
- Profile data stored as a JSON `profile_layout` column: ordered list of widget types + config
**Standard Fields (always present):**
- [ ] Avatar + display name + handle (non-removable header)
- [ ] Bio (rich text, links, emoji)
- [ ] Pronouns field
- [ ] Location (optional, city-level)
**Widget Catalog (user picks and arranges):**
- [ ] **Pinned Posts** — Pin up to 3 posts to the top of your profile
- [ ] **Music Widget** — Currently listening / favorite track (Spotify/Apple Music embed or manual)
- [ ] **Photo Grid** — Mini gallery (3-6 featured photos from uploads)
- [ ] **Social Links** — Icons row for external links (site, GitHub, IG, etc.)
- [ ] **Causes I Care About** — Tag-style badges (environment, mutual aid, arts, etc.)
- [ ] **Featured Friends** — Highlight 3-6 people (like MySpace Top 8 but chill)
- [ ] **Stats Widget** — Post count, follower count, member since (opt-in)
- [ ] **Quote Widget** — A single styled quote / motto
- [ ] **Beacon Activity** — Recent community contributions
- [ ] **Custom Text Block** — Markdown-rendered freeform section
**Theming (constrained but expressive):**
- [ ] Accent color picker (applies to profile header, widget borders, link color)
- [ ] Light/dark/auto profile theme (independent of app theme)
- [ ] Banner image (behind header area)
- [ ] Profile badges (verified, early adopter, community helper — system-assigned)
**Implementation:**
- [ ] Backend: `profile_layout JSONB` column on `profiles` table
- [ ] Backend: `PUT /profile/layout` endpoint to save widget arrangement
- [ ] Frontend: `ProfileWidgetRenderer` that reads layout JSON and renders widget stack
- [ ] Frontend: `ProfileEditor` with drag-to-reorder and add/remove widget catalog
- [ ] Widget sandboxing — each widget has max height, no custom CSS/HTML injection
- [ ] Default layout for new users (bio + social links + pinned posts)
---
### 5. Blocking System
**Status**: Basic block exists. Import/export not implemented.
- [ ] Verify block prevents: seeing posts, DMs, mentions, search results, follow
- [ ] Block list management screen (view, unblock)
- [ ] Export block list as JSON/CSV
- [ ] Import block list from JSON/CSV
- [ ] Import block list from other platforms (Twitter/X format, Mastodon format)
- [ ] Blocked users cannot see your profile or posts
- [ ] Silent block (user doesn't know they're blocked)
**Frontend**: `sojorn_app/lib/screens/profile/blocked_users_screen.dart`
**Backend**: `go-backend/internal/handlers/user_handler.go`
---
### 6. E2EE Chat Stability & Sync
**Status**: X3DH implementation works but key sync across devices is fragile.
- [ ] Audit key recovery flow — ensure it reliably recovers from MAC errors
- [ ] Device-to-device key sync without storing plaintext on server
- [ ] QR code key verification between users
- [ ] "Encrypted with old keys" messages should offer re-request option
- [ ] Clean up `forceResetBrokenKeys()` dead code in `simple_e2ee_service.dart`
- [ ] Ensure cloud backup/restore cycle works end-to-end
- [ ] Add key fingerprint display in chat settings
- [ ] Rate limit key recovery to prevent loops
---
### 7. Repost / Boost Feature
**Status**: Not started.
A "repost" action that amplifies content to your followers without quote-posting.
- [ ] Repost button on posts (share to your followers' feeds)
- [ ] Repost count displayed on posts
- [ ] Reposted-by attribution in feed ("@user reposted")
- [ ] Undo repost
- [ ] Backend: `reposts` table (user_id, post_id, created_at)
- [ ] Feed algorithm weights reposts into feed ranking
---
### 8. Algorithm Refactor — Promote Good, Discourage Anger
**Status**: Basic algorithm exists in `algorithm_config` table. Needs philosophical overhaul.
Core principle: **Show users what they love, not what they hate.**
- [ ] Engagement scoring that weights positive interactions (save, repost, thoughtful reply) over rage-clicks
- [ ] De-rank content with high negative-reaction ratios
- [ ] "Cooling period" — delay viral anger content by 30min before amplifying
- [ ] Boost content tagged as good news, community, mutual aid, creativity
- [ ] User-controllable feed preferences ("show me more of X, less of Y")
- [ ] Diversity injection — prevent echo chambers by mixing in adjacent-interest content
- [ ] Transparency: show users why a post appeared in their feed
- [ ] Admin algorithm tuning panel (already built in admin dashboard)
- [ ] A/B testing framework for algorithm changes
---
## 🔧 Low Priority — Polish & Cleanup
### 9. Remaining Code TODOs
Small scattered items across the codebase:
- [ ] `sojorn_rich_text.dart` — Implement profile navigation from @mentions
- [ ] `post_with_video_widget.dart` — Implement post options menu (edit, delete, report)
- [ ] `video_player_with_comments.dart` — Implement "more options" button
- [ ] `sojorn_swipeable_post.dart` — Wire up allowChain setting when API supports it
- [ ] `reading_post_card.dart` — Implement share functionality
### 10. Legacy Cleanup
- [ ] Delete `go-backend/cmd/supabase-migrate/` directory (dead migration tool)
- [ ] Update 2 stale Supabase comments in `go-backend/internal/middleware/auth.go`
- [ ] Remove `forceResetBrokenKeys()` from `simple_e2ee_service.dart`
---
## ✅ Completed
### Core Platform (shipped)
- ✅ Go backend — 100% migrated from Supabase
- ✅ User auth (JWT, refresh tokens, email verification)
- ✅ Posts (create, edit, delete, visibility, chains)
- ✅ Comments (threaded, with replies)
- ✅ Feed algorithm (basic version)
- ✅ Image/video uploads to Cloudflare R2
- ✅ Follow/unfollow system - ✅ Follow/unfollow system
- ✅ E2EE chat (X3DH implementation) - ✅ Search (users, posts, hashtags)
- ✅ Push notifications (FCM)
- ✅ Search functionality
- ✅ Categories and user settings - ✅ Categories and user settings
- ✅ Chain posts functionality - ✅ Chain posts
- ✅ Beacon voting (vouch, report, remove vote)
- ✅ E2EE chat (X3DH, key backup/restore)
- ✅ Push notifications (FCM)
- ✅ Quips (basic video recording and feed)
### **Infrastructure** ### Admin & Moderation (shipped)
- ✅ Go backend API with all core endpoints - ✅ Admin panel (Next.js dashboard, users, posts, moderation queue, appeals)
- ✅ PostgreSQL database with proper schema - ✅ OpenAI text moderation (auto-flag on post/comment creation)
- ✅ Cloudflare R2 integration for media - ✅ Three Poisons scoring (Hate, Greed, Delusion)
- ✅ Nginx reverse proxy - ✅ Ban/suspend system with content jailing
- ✅ SSL/TLS configuration - ✅ Email notifications (ban, suspend, restore, content removal)
- ✅ Moderation queue with dismiss/action/ban workflows
- ✅ User violation tracking and history
- ✅ IP-based ban evasion detection
### Infrastructure (shipped)
- ✅ PostgreSQL with full schema
- ✅ Cloudflare R2 media storage
- ✅ Nginx reverse proxy + SSL/TLS
- ✅ Systemd service management - ✅ Systemd service management
- ✅ GeoIP for location features
- ✅ Automated deploy scripts
--- ### Recent Fixes (Feb 2026)
- ✅ Notification badge count clears on archive
## 📊 **Current Status Analysis** - ✅ Notification UI → full page (was slide-up dialog)
- ✅ Moderation queue constraint fix (dismissed/actioned statuses)
### **What's Working Better Than Expected** - ✅ Debug print cleanup (190+ statements removed)
- **Comment System**: Fully implemented with Go backend (TODO was outdated) - ✅ Run scripts cleanup (run_dev, run_web, run_web_chrome)
- **Media Uploads**: Direct to R2, no Supabase dependency
- **Chain Posts**: Complete implementation
- **E2EE Chat**: Production-ready X3DH system
### **What Actually Needs Work**
- **Beacon Voting**: Only 3 missing endpoints (core feature)
- **Supabase Cleanup**: Mostly removing legacy code
- **UI Polish**: Fixing outdated TODO comments
### **Key Insight**
The codebase is **90% complete** for Go backend migration. Most TODO comments are outdated and refer to features that are already implemented. The beacon voting system is the only critical missing piece.
---
## 🚀 **Implementation Plan**
### **Day 1: Beacon Voting (4 hours)**
1. Implement beacon voting handlers (2h)
2. Add API routes (30m)
3. Update Flutter API methods (1h)
4. Test beacon voting flow (30m)
### **Day 2: Supabase Cleanup (3 hours)**
1. Remove function proxy (1h)
2. Clean up dependencies (2h)
### **Day 3: UI Polish (3 hours)**
1. Fix video comments TODO (1h)
2. Implement reply features (2h)
### **Day 4: Final Polish & Testing (2 hours)**
1. Add post options menu (1h)
2. End-to-end testing (1h)
---
## 🎯 **Success Criteria**
### **100% Go Backend Functionality**
- [ ] All features work without Supabase
- [ ] Beacon voting system operational
- [ ] No Supabase code or dependencies
- [ ] All TODO comments resolved or updated
- [ ] End-to-end testing passes
### **Code Quality**
- [ ] No debug code in production
- [ ] No outdated comments
- [ ] Clean, maintainable code
- [ ] Proper error handling
- [ ] Security best practices
---
## 📝 **Notes**
- **Most TODO comments are outdated** - features are already implemented
- **Beacon voting is the only critical missing feature**
- **Supabase cleanup is mostly removing legacy code**
- **UI polish items are nice-to-have, not blocking**
**Total estimated effort: ~12 hours to reach 100% Go backend functionality**
---
**Next Steps**: Start with high-priority beacon voting implementation, as it's the only critical missing feature for complete Go backend functionality.