Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
grdsdev committed Jan 4, 2024
1 parent af9fc3e commit e455d01
Show file tree
Hide file tree
Showing 19 changed files with 653 additions and 363 deletions.
4 changes: 4 additions & 0 deletions Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
796298992AEBBA77000AA957 /* MFAFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796298982AEBBA77000AA957 /* MFAFlow.swift */; };
7962989D2AEBC6F9000AA957 /* SVGView in Frameworks */ = {isa = PBXBuildFile; productRef = 7962989C2AEBC6F9000AA957 /* SVGView */; };
79719ECE2ADF26C400737804 /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 79719ECD2ADF26C400737804 /* Supabase */; };
797D664A2B46A1D8007592ED /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 797D66492B46A1D8007592ED /* Store.swift */; };
7993B8A92B3C673A009B610B /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993B8A82B3C673A009B610B /* AuthView.swift */; };
7993B8AB2B3C67E0009B610B /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7993B8AA2B3C67E0009B610B /* Toast.swift */; };
79AF047F2B2CE207008761AD /* AuthWithEmailAndPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF047E2B2CE207008761AD /* AuthWithEmailAndPassword.swift */; };
Expand Down Expand Up @@ -77,6 +78,7 @@
795640692955AFBD0088A06F /* ErrorText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorText.swift; sourceTree = "<group>"; };
796298982AEBBA77000AA957 /* MFAFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFAFlow.swift; sourceTree = "<group>"; };
7962989A2AEBBD9F000AA957 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
797D66492B46A1D8007592ED /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
7993B8A82B3C673A009B610B /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
7993B8AA2B3C67E0009B610B /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = "<group>"; };
7993B8AC2B3C97B6009B610B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -229,6 +231,7 @@
79D884DE2B3C19420009EA4A /* MessagesAPI.swift */,
7993B8A82B3C673A009B610B /* AuthView.swift */,
7993B8AA2B3C67E0009B610B /* Toast.swift */,
797D66492B46A1D8007592ED /* Store.swift */,
);
path = SlackClone;
sourceTree = "<group>";
Expand Down Expand Up @@ -444,6 +447,7 @@
7993B8A92B3C673A009B610B /* AuthView.swift in Sources */,
7993B8AB2B3C67E0009B610B /* Toast.swift in Sources */,
79D884DD2B3C19320009EA4A /* MessagesView.swift in Sources */,
797D664A2B46A1D8007592ED /* Store.swift in Sources */,
79D884DB2B3C191F0009EA4A /* ChannelListView.swift in Sources */,
79D884CC2B3C18830009EA4A /* AppView.swift in Sources */,
79D884D72B3C18DB0009EA4A /* Supabase.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions Examples/SlackClone/AppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ final class AppViewModel {
}
}

@MainActor
struct AppView: View {
let model: AppViewModel

let store = Store()

@ViewBuilder
var body: some View {
if model.session != nil {
ChannelListView()
.environment(store)
} else {
AuthView()
}
Expand Down
26 changes: 5 additions & 21 deletions Examples/SlackClone/ChannelListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,23 @@

import SwiftUI

@Observable
@MainActor
final class ChannelListModel {
var channels: [Channel] = []

func loadChannels() {
Task {
do {
channels = try await supabase.database.from("channels").select().execute().value
} catch {
dump(error)
}
}
}
}

@MainActor
struct ChannelListView: View {
let model = ChannelListModel()
@Environment(Store.self) var store

var body: some View {
NavigationStack {
List {
ForEach(model.channels) { channel in
ForEach(store.channels) { channel in
NavigationLink(channel.slug, value: channel)
}
}
.navigationDestination(for: Channel.self) {
MessagesView(model: MessagesViewModel(channel: $0))
MessagesView(channel: $0)
}
.navigationTitle("Channels")
.onAppear {
model.loadChannels()
.task {
try! await store.loadInitialDataAndSetUpListeners()
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Examples/SlackClone/MessagesAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation
import Supabase

struct User: Codable {
struct User: Codable, Identifiable {
var id: UUID
var username: String
}
Expand All @@ -27,7 +27,7 @@ struct Message: Identifiable, Decodable {
var channel: Channel
}

struct NewMessage: Encodable {
struct NewMessage: Codable {
var message: String
var userId: UUID
let channelId: Int
Expand Down
198 changes: 31 additions & 167 deletions Examples/SlackClone/MessagesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,160 +9,25 @@ import Realtime
import Supabase
import SwiftUI

@Observable
@MainActor
final class MessagesViewModel {
let channel: Channel
var messages: [Message] = []
var newMessage = ""
var presences: [UserPresence] = []

let api: MessagesAPI

init(channel: Channel, api: MessagesAPI = MessagesAPIImpl(supabase: supabase)) {
self.channel = channel
self.api = api

supabase.realtime.logger = { print($0) }
}

func loadInitialMessages() {
Task {
do {
messages = try await api.fetchAllMessages(for: channel.id)
} catch {
dump(error)
}
}
}

private var realtimeChannelV2: RealtimeChannelV2?
private var observationTask: Task<Void, Never>?

func startObservingNewMessages() {
realtimeChannelV2 = supabase.realtimeV2.channel("messages:\(channel.id)")

let messagesChanges = realtimeChannelV2!.postgresChange(
AnyAction.self,
schema: "public",
table: "messages",
filter: "channel_id=eq.\(channel.id)"
)

let presenceChange = realtimeChannelV2!.presenceChange()

observationTask = Task {
await realtimeChannelV2!.subscribe(blockUntilSubscribed: true)

let state = try? await UserPresence(userId: supabase.auth.session.user.id, onlineAt: Date())

try? await realtimeChannelV2!.track(state)

Task {
for await change in messagesChanges {
do {
switch change {
case let .insert(record):
let message = try await self.message(from: record)
self.messages.append(message)

case let .update(record):
let message = try await self.message(from: record)

if let index = self.messages.firstIndex(where: { $0.id == message.id }) {
messages[index] = message
} else {
messages.append(message)
}

case let .delete(oldRecord):
let id = oldRecord.oldRecord["id"]?.intValue
self.messages.removeAll { $0.id == id }

default:
break
}
} catch {
dump(error)
}
}
}

Task {
for await change in presenceChange {
let presences = try change.decodeJoins(as: UserPresence.self)
self.presences = presences
}
}
}
}

func stopObservingMessages() {
Task {
observationTask?.cancel()
await realtimeChannelV2?.untrack()
await realtimeChannelV2?.unsubscribe()
}
}

func submitNewMessageButtonTapped() {
Task {
do {
try await api.insertMessage(
NewMessage(
message: newMessage,
userId: supabase.auth.session.user.id,
channelId: channel.id
)
)
} catch {
dump(error)
}
}
}

private func message(from record: HasRecord) async throws -> Message {
struct MessagePayload: Decodable {
let id: Int
let message: String
let insertedAt: Date
let authorId: UUID
let channelId: UUID
}

let message = try record.decodeRecord() as MessagePayload

return try await Message(
id: message.id,
insertedAt: message.insertedAt,
message: message.message,
user: user(for: message.authorId),
channel: channel
)
}

private var users: [UUID: User] = [:]
private func user(for id: UUID) async throws -> User {
if let user = users[id] { return user }

let user = try await supabase.database.from("users").select().eq("id", value: id).execute()
.value as User
users[id] = user
return user
}
}

struct UserPresence: Codable {
var userId: UUID
var onlineAt: Date
}

@MainActor
struct MessagesView: View {
@Bindable var model: MessagesViewModel
@Environment(Store.self) var store

let channel: Channel
@State private var newMessage = ""

var messages: [Message] {
store.messages[channel.id, default: []]
}

var body: some View {
List {
ForEach(model.messages) { message in
ForEach(messages) { message in
VStack(alignment: .leading) {
Text(message.user.username)
.font(.caption)
Expand All @@ -172,25 +37,32 @@ struct MessagesView: View {
}
}
.safeAreaInset(edge: .bottom) {
ComposeMessageView(text: $model.newMessage) {
model.submitNewMessageButtonTapped()
ComposeMessageView(text: $newMessage) {
Task {
try! await submitNewMessageButtonTapped()
}
}
.padding()
}
.navigationTitle(model.channel.slug)
.toolbar {
ToolbarItem(placement: .principal) {
Text("\(model.presences.count) online")
}
}
.onAppear {
model.loadInitialMessages()
model.startObservingNewMessages()
}
.onDisappear {
model.stopObservingMessages()
.navigationTitle(channel.slug)
// .toolbar {
// ToolbarItem(placement: .principal) {
// Text("\(model.presences.count) online")
// }
// }
.task {
await store.loadInitialMessages(channel.id)
}
}

private func submitNewMessageButtonTapped() async throws {
let message = try await NewMessage(
message: newMessage,
userId: supabase.auth.session.user.id, channelId: channel.id
)

try await supabase.database.from("messages").insert(message).execute()
}
}

struct ComposeMessageView: View {
Expand All @@ -208,11 +80,3 @@ struct ComposeMessageView: View {
}
}
}

#Preview {
MessagesView(model: MessagesViewModel(channel: Channel(
id: 1,
slug: "public",
insertedAt: Date()
)))
}
Loading

0 comments on commit e455d01

Please sign in to comment.