Skip to content

Commit

Permalink
Spec complete for Presence in line with [1].
Browse files Browse the repository at this point in the history
  • Loading branch information
umair-ably committed Nov 18, 2024
1 parent 609ea6d commit 1696f95
Show file tree
Hide file tree
Showing 15 changed files with 594 additions and 67 deletions.
4 changes: 2 additions & 2 deletions AblyChat.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"originHash" : "a296396707b7685153f4cf548f6281f483d562002fe11235f1fc3bb053be91d7",
"originHash" : "1ad2d7338668d15feccbf564582941161acd47349bfca8f34374e11c69677ae8",
"pins" : [
{
"identity" : "ably-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ably/ably-cocoa",
"state" : {
"branch" : "main",
"revision" : "4856ba6a423788902a6ef680793e7f404ceb4a51"
"revision" : "f7bff4b1c941b4c7b952b9224a33674e2302e19f"
}
},
{
Expand Down
25 changes: 20 additions & 5 deletions Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ struct ContentView: View {

private func room() async throws -> Room {
let chosenChatClient: ChatClient = (mode == .mock) ? mockChatClient : liveChatClient
return try await chosenChatClient.rooms.get(roomID: roomID, options: .init(reactions: .init()))
return try await chosenChatClient.rooms.get(
roomID: roomID,
options: .init(
presence: .init(),
reactions: .init(),
occupancy: .init()
)
)
}

private var sendTitle: String {
Expand Down Expand Up @@ -127,12 +134,14 @@ struct ContentView: View {
.tryTask { try await attachRoom() }
.tryTask { try await showMessages() }
.tryTask { try await showReactions() }
.tryTask { try await showPresence() }
.tryTask {
try await showOccupancy()
}
.tryTask {
// NOTE: As we implement more features, move them out of the `if mode == .mock` block and into the main block just above.
if mode == .mock {
try await showPresence()
try await showTypings()
try await showOccupancy()
try await showRoomStatus()
}
}
Expand Down Expand Up @@ -185,9 +194,15 @@ struct ContentView: View {
}

func showPresence() async throws {
for await event in try await room().presence.subscribe(events: [.enter, .leave]) {
try await room().presence.enter(data: .init(userCustomData: ["status": .string("📱 Online")]))

for await event in try await room().presence.subscribe(events: [.enter, .leave, .update]) {
withAnimation {
messages.insert(BasicListItem(id: UUID().uuidString, title: "System", text: event.clientID + " \(event.action.displayedText)"), at: 0)
let status = event.data?.userCustomData?["status"]?.value as? String
let clientPresenceChangeMessage = "\(event.clientID) \(event.action.displayedText)"
let presenceMessage = status != nil ? "\(clientPresenceChangeMessage) with status: \(status!)" : clientPresenceChangeMessage

messages.insert(BasicListItem(id: UUID().uuidString, title: "System", text: presenceMessage), at: 0)
}
}
}
Expand Down
37 changes: 8 additions & 29 deletions Example/AblyChatExample/Mocks/MockClients.swift
Original file line number Diff line number Diff line change
Expand Up @@ -277,19 +277,19 @@ actor MockPresence: Presence {
MockStrings.names.shuffled().map { name in
PresenceMember(
clientID: name,
data: ["foo": "bar"],
data: PresenceData(userCustomData: nil),
action: .present,
extras: nil,
updatedAt: Date()
)
}
}

func get(params _: PresenceQuery?) async throws -> [PresenceMember] {
func get(params _: PresenceQuery) async throws -> [PresenceMember] {
MockStrings.names.shuffled().map { name in
PresenceMember(
clientID: name,
data: ["foo": "bar"],
data: PresenceData(userCustomData: nil),
action: .present,
extras: nil,
updatedAt: Date()
Expand All @@ -301,20 +301,7 @@ actor MockPresence: Presence {
fatalError("Not yet implemented")
}

func enter() async throws {
for subscription in mockSubscriptions {
subscription.emit(
PresenceEvent(
action: .enter,
clientID: clientID,
timestamp: Date(),
data: nil
)
)
}
}

func enter(data: PresenceData) async throws {
func enter(data: PresenceData? = nil) async throws {
for subscription in mockSubscriptions {
subscription.emit(
PresenceEvent(
Expand All @@ -327,28 +314,20 @@ actor MockPresence: Presence {
}
}

func update() async throws {
fatalError("Not yet implemented")
}

func update(data _: PresenceData) async throws {
fatalError("Not yet implemented")
}

func leave() async throws {
func update(data: PresenceData? = nil) async throws {
for subscription in mockSubscriptions {
subscription.emit(
PresenceEvent(
action: .leave,
action: .update,
clientID: clientID,
timestamp: Date(),
data: nil
data: data
)
)
}
}

func leave(data: PresenceData) async throws {
func leave(data: PresenceData? = nil) async throws {
for subscription in mockSubscriptions {
subscription.emit(
PresenceEvent(
Expand Down
4 changes: 4 additions & 0 deletions Example/AblyChatExample/Mocks/MockRealtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ final class MockRealtime: NSObject, RealtimeClientProtocol, Sendable {
fatalError("Not implemented")
}

var presence: ARTRealtimePresenceProtocol {
fatalError("Not implemented")
}

var errorReason: ARTErrorInfo? {
fatalError("Not implemented")
}
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"originHash" : "f00ee2e8c80adfe8d72deb089738cdb967aeae43e71837f90d99fc602728fe45",
"originHash" : "b6d25f160b01b473629481d68d4fe734b3981fcd87079531f784c2ade3afdc4d",
"pins" : [
{
"identity" : "ably-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ably/ably-cocoa",
"state" : {
"branch" : "main",
"revision" : "4856ba6a423788902a6ef680793e7f404ceb4a51"
"revision" : "f7bff4b1c941b4c7b952b9224a33674e2302e19f"
}
},
{
Expand Down
50 changes: 50 additions & 0 deletions Sources/AblyChat/DefaultOccupancy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Ably

internal final class DefaultOccupancy: Occupancy, EmitsDiscontinuities {
private let chatAPI: ChatAPI
private let roomID: String
private let logger: InternalLogger
public nonisolated let featureChannel: FeatureChannel

internal nonisolated var channel: any RealtimeChannelProtocol {
featureChannel.channel
}

internal init(featureChannel: FeatureChannel, chatAPI: ChatAPI, roomID: String, logger: InternalLogger) {
self.featureChannel = featureChannel
self.chatAPI = chatAPI
self.roomID = roomID
self.logger = logger
}

// (CHA-04a) Users may register a listener that receives occupancy events in realtime.
// (CHA-04c) When a regular occupancy event is received on the channel (a standard PubSub occupancy event per the docs), the SDK will convert it into occupancy event format and broadcast it to subscribers.
internal func subscribe(bufferingPolicy: BufferingPolicy) async -> Subscription<OccupancyEvent> {
logger.log(message: "Subscribing to occupancy events", level: .debug)
let subscription = Subscription<OccupancyEvent>(bufferingPolicy: bufferingPolicy)
channel.subscribe("[meta]occupancy") { [logger] message in
logger.log(message: "Received occupancy message: \(message)", level: .debug)
let data = message.data as? [String: Any] ?? [:]
let metrics = data["metrics"] as? [String: Any] ?? [:]

let data1 = metrics["connections"] as? Int ?? 0
let data2 = metrics["presenceMembers"] as? Int ?? 0

let occupancyEvent = OccupancyEvent(connections: data1, presenceMembers: data2)
logger.log(message: "Emitting occupancy event: \(occupancyEvent)", level: .debug)
subscription.emit(occupancyEvent)
}
return subscription
}

// (CHA-O3) Users can request an instantaneous occupancy check via the REST API. The request is detailed here (https://sdk.ably.com/builds/ably/specification/main/chat-features/#rest-occupancy-request), with the response format being a simple occupancy event
internal func get() async throws -> OccupancyEvent {
logger.log(message: "Getting occupancy for room: \(roomID)", level: .debug)
return try await chatAPI.getOccupancy(roomId: roomID)
}

// (CHA-O5) Users may subscribe to discontinuity events to know when there’s been a break in occupancy. Their listener will be called when a discontinuity event is triggered from the room lifecycle. For occupancy, there shouldn’t need to be user action as most channels will send occupancy updates regularly as clients churn.
internal func subscribeToDiscontinuities() async -> Subscription<ARTErrorInfo> {
await featureChannel.subscribeToDiscontinuities()
}
}
Loading

0 comments on commit 1696f95

Please sign in to comment.