Fix notifications: add archived_at to model/queries, archived tab returns only archived, add bottom nav to notifications screen
This commit is contained in:
parent
46566f394b
commit
8186e9e71c
|
|
@ -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"`
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue