Initial commit: nightly iOS app + Supabase backend
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>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user