## Phase 1: Critical Feature Completion (Beacon Voting) - Add VouchBeacon, ReportBeacon, RemoveBeaconVote methods to PostRepository - Implement beacon voting HTTP handlers with confidence score calculations - Register new beacon routes: /beacons/:id/vouch, /beacons/:id/report, /beacons/:id/vouch (DELETE) - Auto-flag beacons at 5+ reports, confidence scoring (0.5 base + 0.1 per vouch) ## Phase 2: Feed Logic & Post Distribution Integrity - Verify unified feed logic supports all content types (Standard, Quips, Beacons) - Ensure proper distribution: Profile Feed + Main/Home Feed for followers - Beacon Map integration for location-based content - Video content filtering for Quips feed ## Phase 3: The Notification System - Create comprehensive NotificationService with FCM integration - Add CreateNotification method to NotificationRepository - Implement smart deep linking: beacon_map, quip_feed, main_feed - Trigger notifications for beacon interactions and cross-post comments - Push notification logic with proper content type detection ## Phase 4: The Great Supabase Purge - Delete function_proxy.go and remove /functions/:name route - Remove SupabaseURL, SupabaseKey from config.go - Remove SupabaseID field from User model - Clean all Supabase imports and dependencies - Sanitize codebase of legacy Supabase references ## Phase 5: Flutter Frontend Integration - Implement vouchBeacon(), reportBeacon(), removeBeaconVote() in ApiService - Replace TODO delay in video_comments_sheet.dart with actual publishComment call - Fix compilation errors (named parameters, orphaned child properties) - Complete frontend integration with Go API endpoints ## Additional Improvements - Fix compilation errors in threaded_comment_widget.dart (orphaned child property) - Update video_comments_sheet.dart to use proper named parameters - Comprehensive error handling and validation - Production-ready notification system with deep linking ## Migration Status: 100% Complete - Backend: Fully migrated from Supabase to custom Go/Gin API - Frontend: Integrated with new Go endpoints - Notifications: Complete FCM integration with smart routing - Database: Clean of all Supabase dependencies - Features: All functionality preserved and enhanced Ready for VPS deployment and production testing!
149 lines
3.9 KiB
Go
149 lines
3.9 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/patbritton/sojorn-backend/internal/models"
|
|
"github.com/patbritton/sojorn-backend/internal/repository"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type NotificationService struct {
|
|
notifRepo *repository.NotificationRepository
|
|
pushSvc *PushService
|
|
}
|
|
|
|
func NewNotificationService(notifRepo *repository.NotificationRepository, pushSvc *PushService) *NotificationService {
|
|
return &NotificationService{
|
|
notifRepo: notifRepo,
|
|
pushSvc: pushSvc,
|
|
}
|
|
}
|
|
|
|
func (s *NotificationService) CreateNotification(ctx context.Context, userID, actorID, notificationType string, postID *string, commentID *string, metadata map[string]interface{}) error {
|
|
// Parse UUIDs
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid user ID: %w", err)
|
|
}
|
|
|
|
actorUUID, err := uuid.Parse(actorID)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid actor ID: %w", err)
|
|
}
|
|
|
|
// Create database notification
|
|
notif := &models.Notification{
|
|
UserID: userUUID,
|
|
ActorID: actorUUID,
|
|
Type: notificationType,
|
|
PostID: parseNullableUUID(postID),
|
|
CommentID: parseNullableUUID(commentID),
|
|
IsRead: false,
|
|
}
|
|
|
|
// Serialize metadata
|
|
if metadata != nil {
|
|
metadataBytes, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to marshal notification metadata")
|
|
} else {
|
|
notif.Metadata = metadataBytes
|
|
}
|
|
}
|
|
|
|
// Insert into database
|
|
err = s.notifRepo.CreateNotification(ctx, notif)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create notification: %w", err)
|
|
}
|
|
|
|
// Send push notification
|
|
if s.pushSvc != nil {
|
|
title, body, data := s.buildPushNotification(notificationType, metadata)
|
|
if err := s.pushSvc.SendPush(ctx, userID, title, body, data); err != nil {
|
|
log.Warn().Err(err).Str("user_id", userID).Msg("Failed to send push notification")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *NotificationService) buildPushNotification(notificationType string, metadata map[string]interface{}) (title, body string, data map[string]string) {
|
|
actorName, _ := metadata["actor_name"].(string)
|
|
|
|
switch notificationType {
|
|
case "beacon_vouch":
|
|
title = "Beacon Vouched"
|
|
body = fmt.Sprintf("%s vouched for your beacon", actorName)
|
|
data = map[string]string{
|
|
"type": "beacon_vouch",
|
|
"beacon_id": getString(metadata, "beacon_id"),
|
|
"target": "beacon_map", // Deep link to map
|
|
}
|
|
case "beacon_report":
|
|
title = "Beacon Reported"
|
|
body = fmt.Sprintf("%s reported your beacon", actorName)
|
|
data = map[string]string{
|
|
"type": "beacon_report",
|
|
"beacon_id": getString(metadata, "beacon_id"),
|
|
"target": "beacon_map", // Deep link to map
|
|
}
|
|
case "comment":
|
|
title = "New Comment"
|
|
postType := getString(metadata, "post_type")
|
|
if postType == "beacon" {
|
|
body = fmt.Sprintf("%s commented on your beacon", actorName)
|
|
data = map[string]string{
|
|
"type": "comment",
|
|
"post_id": getString(metadata, "post_id"),
|
|
"target": "beacon_map", // Deep link to map for beacon comments
|
|
}
|
|
} else if postType == "quip" {
|
|
body = fmt.Sprintf("%s commented on your quip", actorName)
|
|
data = map[string]string{
|
|
"type": "comment",
|
|
"post_id": getString(metadata, "post_id"),
|
|
"target": "quip_feed", // Deep link to quip feed
|
|
}
|
|
} else {
|
|
body = fmt.Sprintf("%s commented on your post", actorName)
|
|
data = map[string]string{
|
|
"type": "comment",
|
|
"post_id": getString(metadata, "post_id"),
|
|
"target": "main_feed", // Deep link to main feed
|
|
}
|
|
}
|
|
default:
|
|
title = "Sojorn"
|
|
body = "You have a new notification"
|
|
data = map[string]string{"type": notificationType}
|
|
}
|
|
|
|
return title, body, data
|
|
}
|
|
|
|
// Helper functions
|
|
func parseNullableUUID(s *string) *uuid.UUID {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
u, err := uuid.Parse(*s)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &u
|
|
}
|
|
|
|
func getString(m map[string]interface{}, key string) string {
|
|
if val, ok := m[key]; ok {
|
|
if str, ok := val.(string); ok {
|
|
return str
|
|
}
|
|
}
|
|
return ""
|
|
}
|