diff --git a/.swiftlint.yml b/.swiftlint.yml index 967c45bf..f8503f90 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -77,7 +77,6 @@ opt_in_rules: # Opt-in rules of type "lint" that we’ve decided we want: - array_init - empty_xctest_method - - missing_docs - override_in_extension - yoda_condition - private_swiftui_state diff --git a/Example/AblyChatExample.xcodeproj/project.pbxproj b/Example/AblyChatExample.xcodeproj/project.pbxproj index 685c63a9..a704d8b1 100644 --- a/Example/AblyChatExample.xcodeproj/project.pbxproj +++ b/Example/AblyChatExample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 212F95A72C6CAD9300420287 /* MockRealtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212F95A62C6CAD9300420287 /* MockRealtime.swift */; }; 21971DFF2C60D89C0074B8AE /* AblyChat in Frameworks */ = {isa = PBXBuildFile; productRef = 21971DFE2C60D89C0074B8AE /* AblyChat */; }; 21F09AA02C60CAF00025AF73 /* AblyChatExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F09A9F2C60CAF00025AF73 /* AblyChatExampleApp.swift */; }; 21F09AA22C60CAF00025AF73 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F09AA12C60CAF00025AF73 /* ContentView.swift */; }; @@ -15,6 +16,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 212F95A62C6CAD9300420287 /* MockRealtime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRealtime.swift; sourceTree = ""; }; 21F09A9C2C60CAF00025AF73 /* AblyChatExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AblyChatExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21F09A9F2C60CAF00025AF73 /* AblyChatExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AblyChatExampleApp.swift; sourceTree = ""; }; 21F09AA12C60CAF00025AF73 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -35,6 +37,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 212F95A52C6CAD7E00420287 /* Mocks */ = { + isa = PBXGroup; + children = ( + 212F95A62C6CAD9300420287 /* MockRealtime.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 21971DFD2C60D89C0074B8AE /* Frameworks */ = { isa = PBXGroup; children = ( @@ -62,6 +72,7 @@ 21F09A9E2C60CAF00025AF73 /* AblyChatExample */ = { isa = PBXGroup; children = ( + 212F95A52C6CAD7E00420287 /* Mocks */, 21F09A9F2C60CAF00025AF73 /* AblyChatExampleApp.swift */, 21F09AA12C60CAF00025AF73 /* ContentView.swift */, 21F09AA32C60CAF20025AF73 /* Assets.xcassets */, @@ -152,6 +163,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 212F95A72C6CAD9300420287 /* MockRealtime.swift in Sources */, 21F09AA22C60CAF00025AF73 /* ContentView.swift in Sources */, 21F09AA02C60CAF00025AF73 /* AblyChatExampleApp.swift in Sources */, ); diff --git a/Example/AblyChatExample/ContentView.swift b/Example/AblyChatExample/ContentView.swift index 6856f825..750c754d 100644 --- a/Example/AblyChatExample/ContentView.swift +++ b/Example/AblyChatExample/ContentView.swift @@ -3,7 +3,10 @@ import SwiftUI struct ContentView: View { /// Just used to check that we can successfully import and use the AblyChat library. TODO remove this once we start building the library - @State private var ablyChatClient = AblyChatClient() + @State private var ablyChatClient = DefaultChatClient( + realtime: MockRealtime(key: ""), + clientOptions: ClientOptions() + ) var body: some View { VStack { diff --git a/Example/AblyChatExample/Mocks/MockRealtime.swift b/Example/AblyChatExample/Mocks/MockRealtime.swift new file mode 100644 index 00000000..8aa0d015 --- /dev/null +++ b/Example/AblyChatExample/Mocks/MockRealtime.swift @@ -0,0 +1,40 @@ +import Ably + +/// A mock implementation of `ARTRealtimeProtocol`. It only exists so that we can construct an instance of `DefaultChatClient` without needing to create a proper `ARTRealtime` instance (which we can’t yet do because we don’t have a method for inserting an API key into the example app). TODO remove this once we start building the example app +class MockRealtime: NSObject, ARTRealtimeProtocol { + var device: ARTLocalDevice { + fatalError("Not implemented") + } + + var clientId: String? + + required init(options _: ARTClientOptions) {} + + required init(key _: String) {} + + required init(token _: String) {} + + func time(_: @escaping ARTDateTimeCallback) { + fatalError("Not implemented") + } + + func ping(_: @escaping ARTCallback) { + fatalError("Not implemented") + } + + func stats(_: @escaping ARTPaginatedStatsCallback) -> Bool { + fatalError("Not implemented") + } + + func stats(_: ARTStatsQuery?, callback _: @escaping ARTPaginatedStatsCallback) throws { + fatalError("Not implemented") + } + + func connect() { + fatalError("Not implemented") + } + + func close() { + fatalError("Not implemented") + } +} diff --git a/Sources/AblyChat/.swiftlint.yml b/Sources/AblyChat/.swiftlint.yml new file mode 100644 index 00000000..b3580378 --- /dev/null +++ b/Sources/AblyChat/.swiftlint.yml @@ -0,0 +1,3 @@ +opt_in_rules: + # Opt-in rules of type "idiomatic" that we’ve decided we want: + - explicit_acl diff --git a/Sources/AblyChat/AblyChat.swift b/Sources/AblyChat/AblyChat.swift deleted file mode 100644 index dc0816df..00000000 --- a/Sources/AblyChat/AblyChat.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Ably - -/// Temporary class just used to check that the example app and tests can use the library. TODO remove this once we start building the library -public class AblyChatClient { - /// Initializes an instance of `AblyChatClient`. - public init() {} -} diff --git a/Sources/AblyChat/BufferingPolicy.swift b/Sources/AblyChat/BufferingPolicy.swift new file mode 100644 index 00000000..2083cd11 --- /dev/null +++ b/Sources/AblyChat/BufferingPolicy.swift @@ -0,0 +1,7 @@ +/// Describes what to do with realtime events that come in faster than the consumer of an `AsyncSequence` can handle them. +/// (This is the same as `AsyncStream.Continuation.BufferingPolicy` but with the generic type parameter `T` removed.) +public enum BufferingPolicy { + case unbounded + case bufferingOldest(Int) + case bufferingNewest(Int) +} diff --git a/Sources/AblyChat/ChatClient.swift b/Sources/AblyChat/ChatClient.swift new file mode 100644 index 00000000..aa919a4d --- /dev/null +++ b/Sources/AblyChat/ChatClient.swift @@ -0,0 +1,45 @@ +import Ably + +public protocol ChatClient: AnyObject, Sendable { + var rooms: any Rooms { get } + var connection: any Connection { get } + var clientID: String { get } + var realtime: any ARTRealtimeProtocol { get } + var clientOptions: ClientOptions { get } +} + +public final class DefaultChatClient: ChatClient { + public init(realtime _: ARTRealtimeProtocol, clientOptions _: ClientOptions?) { + // This one doesn’t do `fatalError`, so that I can call it in the example app + } + + public var rooms: any Rooms { + fatalError("Not yet implemented") + } + + public var connection: any Connection { + fatalError("Not yet implemented") + } + + public var clientID: String { + fatalError("Not yet implemented") + } + + public var realtime: any ARTRealtimeProtocol { + fatalError("Not yet implemented") + } + + public var clientOptions: ClientOptions { + fatalError("Not yet implemented") + } +} + +public struct ClientOptions: Sendable { + public var logHandler: LogHandler? + public var logLevel: LogLevel? + + public init(logHandler: (any LogHandler)? = nil, logLevel: LogLevel? = nil) { + self.logHandler = logHandler + self.logLevel = logLevel + } +} diff --git a/Sources/AblyChat/Connection.swift b/Sources/AblyChat/Connection.swift new file mode 100644 index 00000000..3d2c92d9 --- /dev/null +++ b/Sources/AblyChat/Connection.swift @@ -0,0 +1,34 @@ +import Ably + +public protocol Connection: AnyObject, Sendable { + var status: any ConnectionStatus { get } +} + +public protocol ConnectionStatus: AnyObject, Sendable { + var current: ConnectionLifecycle { get } + var error: ARTErrorInfo? { get } + func subscribe(bufferingPolicy: BufferingPolicy) -> Subscription +} + +public enum ConnectionLifecycle: Sendable { + case initialized + case connecting + case connected + case disconnected + case suspended + case failed +} + +public struct ConnectionStatusChange: Sendable { + public var current: ConnectionLifecycle + public var previous: ConnectionLifecycle + public var error: ARTErrorInfo? + public var retryIn: TimeInterval + + public init(current: ConnectionLifecycle, previous: ConnectionLifecycle, error: ARTErrorInfo? = nil, retryIn: TimeInterval) { + self.current = current + self.previous = previous + self.error = error + self.retryIn = retryIn + } +} diff --git a/Sources/AblyChat/EmitsDiscontinuities.swift b/Sources/AblyChat/EmitsDiscontinuities.swift new file mode 100644 index 00000000..ed3119f7 --- /dev/null +++ b/Sources/AblyChat/EmitsDiscontinuities.swift @@ -0,0 +1,5 @@ +import Ably + +public protocol EmitsDiscontinuities { + func subscribeToDiscontinuities() -> Subscription +} diff --git a/Sources/AblyChat/Headers.swift b/Sources/AblyChat/Headers.swift new file mode 100644 index 00000000..ef7c4d3b --- /dev/null +++ b/Sources/AblyChat/Headers.swift @@ -0,0 +1 @@ +public typealias Headers = Any & Sendable /* TODO: Record; */ diff --git a/Sources/AblyChat/Logging.swift b/Sources/AblyChat/Logging.swift new file mode 100644 index 00000000..d0a9c199 --- /dev/null +++ b/Sources/AblyChat/Logging.swift @@ -0,0 +1,14 @@ +public typealias LogContext = [String: Any] + +public protocol LogHandler: AnyObject, Sendable { + func log(message: String, level: LogLevel, context: LogContext?) +} + +public enum LogLevel: Sendable { + case trace + case debug + case info + case warn + case error + case silent +} diff --git a/Sources/AblyChat/Message.swift b/Sources/AblyChat/Message.swift new file mode 100644 index 00000000..cbbcbdfe --- /dev/null +++ b/Sources/AblyChat/Message.swift @@ -0,0 +1,36 @@ +import Foundation + +public typealias MessageHeaders = Headers +public typealias MessageMetadata = Metadata + +public struct Message: Sendable { + public var timeserial: String + public var clientID: String + public var roomID: String + public var text: String + public var createdAt: Date + public var metadata: MessageMetadata + public var headers: MessageHeaders + + public init(timeserial: String, clientID: String, roomID: String, text: String, createdAt: Date, metadata: any MessageMetadata, headers: any MessageHeaders) { + self.timeserial = timeserial + self.clientID = clientID + self.roomID = roomID + self.text = text + self.createdAt = createdAt + self.metadata = metadata + self.headers = headers + } + + public func isBefore(_: Message) -> Bool { + fatalError("Not yet implemented") + } + + public func isAfter(_: Message) -> Bool { + fatalError("Not yet implemented") + } + + public func isEqual(_: Message) -> Bool { + fatalError("Not yet implemented") + } +} diff --git a/Sources/AblyChat/Messages.swift b/Sources/AblyChat/Messages.swift new file mode 100644 index 00000000..bd6a4edd --- /dev/null +++ b/Sources/AblyChat/Messages.swift @@ -0,0 +1,77 @@ +import Ably + +public protocol Messages: AnyObject, Sendable, EmitsDiscontinuities { + func subscribe(bufferingPolicy: BufferingPolicy) -> MessageSubscription + func get(options: QueryOptions) async throws -> any PaginatedResult + func send(params: SendMessageParams) async throws -> Message + var channel: ARTRealtimeChannelProtocol { get } +} + +public struct SendMessageParams: Sendable { + public var text: String + public var metadata: MessageMetadata? + public var headers: MessageHeaders? + + public init(text: String, metadata: (any MessageMetadata)? = nil, headers: (any MessageHeaders)? = nil) { + self.text = text + self.metadata = metadata + self.headers = headers + } +} + +public struct QueryOptions: Sendable { + public enum Direction: Sendable { + case forwards + case backwards + } + + public var start: Date? + public var end: Date? + public var limit: Int? + public var direction: Direction? + + public init(start: Date? = nil, end: Date? = nil, limit: Int? = nil, direction: QueryOptions.Direction? = nil) { + self.start = start + self.end = end + self.limit = limit + self.direction = direction + } +} + +public struct QueryOptionsWithoutDirection: Sendable { + public var start: Date? + public var end: Date? + public var limit: Int? + + public init(start: Date? = nil, end: Date? = nil, limit: Int? = nil) { + self.start = start + self.end = end + self.limit = limit + } +} + +// TODO: note this will start accumulating messages as soon as created +// TODO: note that I wanted this to instead inherit from Sequence protocol but that's not possible +public struct MessageSubscription: Sendable, AsyncSequence { + public typealias Element = Message + + // TODO: explain, this is a workaround to allow us to write mocks + public init(mockAsyncSequence _: T) where T.Element == Element { + fatalError("Not yet implemented") + } + + public func getPreviousMessages(params _: QueryOptionsWithoutDirection) async throws -> any PaginatedResult { + fatalError("Not yet implemented") + } + + public struct AsyncIterator: AsyncIteratorProtocol { + // note that I’ve removed the `throws` here and that means we don't need a `try` in the loop + public mutating func next() async -> Element? { + fatalError("Not implemented") + } + } + + public func makeAsyncIterator() -> AsyncIterator { + fatalError("Not implemented") + } +} diff --git a/Sources/AblyChat/Metadata.swift b/Sources/AblyChat/Metadata.swift new file mode 100644 index 00000000..5ad14c24 --- /dev/null +++ b/Sources/AblyChat/Metadata.swift @@ -0,0 +1 @@ +public typealias Metadata = Any & Sendable // TODO: Record; diff --git a/Sources/AblyChat/Occupancy.swift b/Sources/AblyChat/Occupancy.swift new file mode 100644 index 00000000..adadc1a0 --- /dev/null +++ b/Sources/AblyChat/Occupancy.swift @@ -0,0 +1,17 @@ +import Ably + +public protocol Occupancy: AnyObject, Sendable, EmitsDiscontinuities { + func subscribe(bufferingPolicy: BufferingPolicy) -> Subscription + func get() async throws -> OccupancyEvent + var channel: ARTRealtimeChannelProtocol { get } +} + +public struct OccupancyEvent { + public var connections: Int + public var presenceMembers: Int + + public init(connections: Int, presenceMembers: Int) { + self.connections = connections + self.presenceMembers = presenceMembers + } +} diff --git a/Sources/AblyChat/PaginatedResult.swift b/Sources/AblyChat/PaginatedResult.swift new file mode 100644 index 00000000..fab47206 --- /dev/null +++ b/Sources/AblyChat/PaginatedResult.swift @@ -0,0 +1,11 @@ +public protocol PaginatedResult: AnyObject, Sendable { + associatedtype T + + var items: [T] { get } + var hasNext: Bool { get } + var isLast: Bool { get } + // TODO: is there a way to link `hasNext` and `next`’s nullability? + var next: (any PaginatedResult)? { get async throws } + var first: any PaginatedResult { get async throws } + var current: Bool { get async throws } +} diff --git a/Sources/AblyChat/Presence.swift b/Sources/AblyChat/Presence.swift new file mode 100644 index 00000000..e6ef36f3 --- /dev/null +++ b/Sources/AblyChat/Presence.swift @@ -0,0 +1,66 @@ +import Ably + +// TODO: the JS one says "Any JSON serializable data type."; what's the best way to represent this in Swift? Maybe would be best done with `Encodable` +public typealias PresenceData = Any & Sendable + +public protocol Presence: AnyObject, Sendable, EmitsDiscontinuities { + func get() async throws -> any PaginatedResult<[PresenceMember]> + func get(params: ARTRealtimePresenceQuery?) async throws -> any PaginatedResult<[PresenceMember]> + func isUserPresent(clientID: String) async throws -> Bool + func enter() async throws + func enter(data: PresenceData) async throws + func update() async throws + func update(data: PresenceData) async throws + func leave() async throws + func leave(data: PresenceData) async throws + func subscribe(event: PresenceEventType) -> Subscription + func subscribe(events: [PresenceEventType]) -> Subscription +} + +public struct PresenceMember: Sendable { + // TODO: why is this defined inline in the JS one? how is it different to its `PresenceEvents` enum (i.e. our PresenceEventsType enum)? + public enum Action: Sendable { + case present + case enter + case leave + case update + } + + public init(clientID: String, data: any PresenceData, action: PresenceMember.Action, extras: any Sendable, updatedAt: Date) { + self.clientID = clientID + self.data = data + self.action = action + self.extras = extras + self.updatedAt = updatedAt + } + + public var clientID: String + // TODO: it’s `unknown` in JS; this probably isn’t equivalent because I guess it could be nil + public var data: PresenceData + public var action: Action + // TODO: what about this? + public var extras: Sendable + public var updatedAt: Date +} + +public enum PresenceEventType: Sendable { + case enter + case leave + case update + case present +} + +// TODO: how is this different to PresenceMember? +public struct PresenceEvent: Sendable { + public var action: PresenceEventType + public var clientID: String + public var timestamp: Date + public var data: PresenceData + + public init(action: PresenceEventType, clientID: String, timestamp: Date, data: any PresenceData) { + self.action = action + self.clientID = clientID + self.timestamp = timestamp + self.data = data + } +} diff --git a/Sources/AblyChat/Reaction.swift b/Sources/AblyChat/Reaction.swift new file mode 100644 index 00000000..c68714ab --- /dev/null +++ b/Sources/AblyChat/Reaction.swift @@ -0,0 +1,22 @@ +import Foundation + +public typealias ReactionHeaders = Headers +public typealias ReactionMetadata = Metadata + +public struct Reaction: Sendable { + public var type: String + public var metadata: ReactionMetadata + public var headers: ReactionHeaders + public var createdAt: Date + public var clientID: String + public var isSelf: Bool + + public init(type: String, metadata: any ReactionMetadata, headers: any ReactionHeaders, createdAt: Date, clientID: String, isSelf: Bool) { + self.type = type + self.metadata = metadata + self.headers = headers + self.createdAt = createdAt + self.clientID = clientID + self.isSelf = isSelf + } +} diff --git a/Sources/AblyChat/Room.swift b/Sources/AblyChat/Room.swift new file mode 100644 index 00000000..81a74ff2 --- /dev/null +++ b/Sources/AblyChat/Room.swift @@ -0,0 +1,12 @@ +public protocol Room: AnyObject, Sendable { + var roomID: String { get } + var messages: any Messages { get } + var presence: any Presence { get throws } + var reactions: any RoomReactions { get throws } + var typing: any Typing { get throws } + var occupancy: any Occupancy { get throws } + var status: any RoomStatus { get } + func attach() async throws + func detach() async throws + var options: RoomOptions { get } +} diff --git a/Sources/AblyChat/RoomOptions.swift b/Sources/AblyChat/RoomOptions.swift new file mode 100644 index 00000000..a927bfc8 --- /dev/null +++ b/Sources/AblyChat/RoomOptions.swift @@ -0,0 +1,41 @@ +import Foundation + +public struct RoomOptions: Sendable { + public var presence: PresenceOptions? + public var typing: TypingOptions? + public var reactions: RoomReactionsOptions? + public var occupancy: OccupancyOptions? + + public init(presence: PresenceOptions? = nil, typing: TypingOptions? = nil, reactions: RoomReactionsOptions? = nil, occupancy: OccupancyOptions? = nil) { + self.presence = presence + self.typing = typing + self.reactions = reactions + self.occupancy = occupancy + } +} + +public struct PresenceOptions: Sendable { + public var enter = true + public var subscribe = true + + public init(enter: Bool = true, subscribe: Bool = true) { + self.enter = enter + self.subscribe = subscribe + } +} + +public struct TypingOptions: Sendable { + public var timeout: TimeInterval = 10 + + public init(timeout: TimeInterval = 10) { + self.timeout = timeout + } +} + +public struct RoomReactionsOptions: Sendable { + public init() {} +} + +public struct OccupancyOptions: Sendable { + public init() {} +} diff --git a/Sources/AblyChat/RoomReactions.swift b/Sources/AblyChat/RoomReactions.swift new file mode 100644 index 00000000..e59e0fc8 --- /dev/null +++ b/Sources/AblyChat/RoomReactions.swift @@ -0,0 +1,11 @@ +import Ably + +public protocol RoomReactions: AnyObject, Sendable, EmitsDiscontinuities { + func send(params: RoomReactionParams) async throws + func subscribe(bufferingPolicy: BufferingPolicy) -> Subscription + var channel: ARTRealtimeChannelProtocol { get } +} + +public struct RoomReactionParams: Sendable { + public init() {} +} diff --git a/Sources/AblyChat/RoomStatus.swift b/Sources/AblyChat/RoomStatus.swift new file mode 100644 index 00000000..4a1f8b0b --- /dev/null +++ b/Sources/AblyChat/RoomStatus.swift @@ -0,0 +1,36 @@ +import Ably + +public protocol RoomStatus: AnyObject, Sendable { + // TODO: questions re API here also apply to ConnectionStatus + + var current: RoomLifecycle { get } + // TODO: should this be part of the RoomLifecycle enum instead? + var error: ARTErrorInfo? { get } + // TODO: is it weird to have a sequence of status changes? + func subscribe(bufferingPolicy: BufferingPolicy) -> Subscription +} + +public enum RoomLifecycle: Sendable { + case initialized + case attaching + case attached + case detaching + case detached + case suspended + case failed + case releasing + case released +} + +public struct RoomStatusChange: Sendable { + public var current: RoomLifecycle + public var previous: RoomLifecycle + // TODO: tie this to the state + public var error: ARTErrorInfo? + + public init(current: RoomLifecycle, previous: RoomLifecycle, error: ARTErrorInfo? = nil) { + self.current = current + self.previous = previous + self.error = error + } +} diff --git a/Sources/AblyChat/Rooms.swift b/Sources/AblyChat/Rooms.swift new file mode 100644 index 00000000..f09478a4 --- /dev/null +++ b/Sources/AblyChat/Rooms.swift @@ -0,0 +1,5 @@ +public protocol Rooms: AnyObject, Sendable { + func get(roomID: String, options: RoomOptions) throws -> any Room + func release(roomID: String) async throws + var clientOptions: ClientOptions { get } +} diff --git a/Sources/AblyChat/Subscription.swift b/Sources/AblyChat/Subscription.swift new file mode 100644 index 00000000..a86fc9e3 --- /dev/null +++ b/Sources/AblyChat/Subscription.swift @@ -0,0 +1,21 @@ +/// TODO: so what's the API here? there needs to be a way of purging the buffer, which I assume is what AsyncStream does when _somebody_ starts to iterate over it. I think this should just work the same as AsyncSequence; that you can have multiple consumers but they both drain it +/// TODO note that I wasn't able to do this as protocols +/// So, this works — both in that you don't need `try` in the loop and that it knows the element type. Why doesn’t it work with protocols? If +/// but this is no good because now we can't really mock this +public struct Subscription: Sendable, AsyncSequence { + // TODO: explain, this is a workaround to allow us to write mocks + public init(mockAsyncSequence _: T) where T.Element == Element { + fatalError("Not implemented") + } + + public struct AsyncIterator: AsyncIteratorProtocol { + // note that I’ve removed the `throws` here and that means we don't need a `try` in the loop + public mutating func next() async -> Element? { + fatalError("Not implemented") + } + } + + public func makeAsyncIterator() -> AsyncIterator { + fatalError("Not implemented") + } +} diff --git a/Sources/AblyChat/Typing.swift b/Sources/AblyChat/Typing.swift new file mode 100644 index 00000000..fdc380ac --- /dev/null +++ b/Sources/AblyChat/Typing.swift @@ -0,0 +1,17 @@ +import Ably + +public protocol Typing: AnyObject, Sendable, EmitsDiscontinuities { + func subscribe(bufferingPolicy: BufferingPolicy) -> Subscription + func get() async throws -> Set + func start() async throws + func stop() async throws + var channel: ARTRealtimeChannelProtocol { get } +} + +public struct TypingEvent: Sendable { + public var currentlyTyping: Set + + public init(currentlyTyping: Set) { + self.currentlyTyping = currentlyTyping + } +} diff --git a/Tests/AblyChatTests/AblyChatTests.swift b/Tests/AblyChatTests/AblyChatTests.swift index 054ea7fe..c8c5c0bc 100644 --- a/Tests/AblyChatTests/AblyChatTests.swift +++ b/Tests/AblyChatTests/AblyChatTests.swift @@ -3,6 +3,6 @@ import XCTest final class AblyChatTests: XCTestCase { func testExample() throws { - XCTAssertNoThrow(AblyChatClient()) + XCTAssertNoThrow(DefaultChatClient(realtime: MockRealtime(key: ""), clientOptions: ClientOptions())) } } diff --git a/Tests/AblyChatTests/Mocks/MockRealtime.swift b/Tests/AblyChatTests/Mocks/MockRealtime.swift new file mode 100644 index 00000000..f49b06c7 --- /dev/null +++ b/Tests/AblyChatTests/Mocks/MockRealtime.swift @@ -0,0 +1,41 @@ +import Ably +import Foundation + +/// A mock implementation of `ARTRealtimeProtocol`. Copied from the class of the same name in the example app. We’ll figure out how to do mocking in tests properly in https://github.com/ably-labs/ably-chat-swift/issues/5. +class MockRealtime: NSObject, ARTRealtimeProtocol { + var device: ARTLocalDevice { + fatalError("Not implemented") + } + + var clientId: String? + + required init(options _: ARTClientOptions) {} + + required init(key _: String) {} + + required init(token _: String) {} + + func time(_: @escaping ARTDateTimeCallback) { + fatalError("Not implemented") + } + + func ping(_: @escaping ARTCallback) { + fatalError("Not implemented") + } + + func stats(_: @escaping ARTPaginatedStatsCallback) -> Bool { + fatalError("Not implemented") + } + + func stats(_: ARTStatsQuery?, callback _: @escaping ARTPaginatedStatsCallback) throws { + fatalError("Not implemented") + } + + func connect() { + fatalError("Not implemented") + } + + func close() { + fatalError("Not implemented") + } +}