5bc81d5b3b
iOS SwiftUI app with Supabase auth/realtime, Node.js backend, Docker/Supabase self-hosted infrastructure, and APNs scheduler. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
87 lines
2.5 KiB
Swift
87 lines
2.5 KiB
Swift
import Foundation
|
|
import Supabase
|
|
|
|
/// Verwaltet die Echtzeit-Verbindung für "Gerade Jetzt".
|
|
/// Neue Posts erscheinen sofort ohne Polling.
|
|
@MainActor
|
|
class RealtimeService: ObservableObject {
|
|
@Published var newPostsCount = 0
|
|
|
|
private var channel: RealtimeChannelV2?
|
|
private var onNewPost: ((Post) -> Void)?
|
|
|
|
func startListening(onNewPost: @escaping (Post) -> Void) async {
|
|
self.onNewPost = onNewPost
|
|
guard channel == nil else { return }
|
|
|
|
let ch = await supabase.channel("public:posts")
|
|
|
|
// Neue Posts in Echtzeit empfangen
|
|
let stream = await ch.postgresChange(
|
|
InsertAction.self,
|
|
schema: "public",
|
|
table: "posts"
|
|
)
|
|
|
|
await ch.subscribe()
|
|
self.channel = ch
|
|
|
|
// Stream im Hintergrund konsumieren
|
|
Task { [weak self] in
|
|
for await action in stream {
|
|
await self?.handleInsert(action)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stopListening() async {
|
|
if let ch = channel {
|
|
await supabase.removeChannel(ch)
|
|
channel = nil
|
|
}
|
|
}
|
|
|
|
private func handleInsert(_ action: InsertAction) {
|
|
// Den neuen Post aus dem Record dekodieren
|
|
guard
|
|
let id = action.record["id"]?.stringValue,
|
|
let content = action.record["content"]?.stringValue,
|
|
let createdAt = action.record["created_at"]?.stringValue
|
|
.flatMap({ ISO8601DateFormatter().date(from: $0) }),
|
|
let userId = action.record["user_id"]?.stringValue,
|
|
let isAnon = action.record["is_anonymous"]?.boolValue
|
|
else { return }
|
|
|
|
let moodString = action.record["mood"]?.stringValue
|
|
let mood = moodString.flatMap(Mood.init(rawValue:))
|
|
|
|
let post = Post(
|
|
id: id,
|
|
author: User.anonymousPlaceholder, // Profil wird lazily nachgeladen
|
|
content: content,
|
|
mood: mood,
|
|
createdAt: createdAt,
|
|
resonanceCount: 0,
|
|
hasResonated: false,
|
|
commentCount: 0,
|
|
isAnonymous: isAnon,
|
|
nightOf: createdAt
|
|
)
|
|
|
|
newPostsCount += 1
|
|
onNewPost?(post)
|
|
}
|
|
}
|
|
|
|
// MARK: - User placeholder für Realtime (Profil wird nachgeladen)
|
|
|
|
extension User {
|
|
static let anonymousPlaceholder = User(
|
|
id: "anonymous",
|
|
username: "anonym",
|
|
displayName: "anonym",
|
|
bio: nil, avatarURL: nil,
|
|
followerCount: 0, followingCount: 0, postCount: 0, isFollowing: false
|
|
)
|
|
}
|