diff --git a/README.md b/README.md index c2ca74b..349c4ec 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,24 @@ # Ably Chat SDK for Swift -This is the repository for the Swift version of the Ably Chat SDK. We aim to build the same functionality that’s available in the [JavaScript SDK](https://github.com/ably/ably-chat-js). +

+ Development status + License + version: 0.1.0-SNAPSHOT +

+ +Ably Chat is a set of purpose-built APIs for a host of chat features enabling you to create 1:1, 1:Many, Many:1 and Many:Many chat rooms for +any scale. It is designed to meet a wide range of chat use cases, such as livestreams, in-game communication, customer support, or social +interactions in SaaS products. Built on [Ably's](https://ably.com/) core service, it abstracts complex details to enable efficient chat +architectures. > [!IMPORTANT] -> This SDK is currently in the early stages of development and is not ready to be used. +> This SDK is currently under development. If you are interested in being an early adopter and providing feedback then you +> can [sign up to the private beta](https://forms.gle/vB2kXhCXrTQpzHLu5) and are welcome +> to [provide us with feedback](https://forms.gle/mBw9M53NYuCBLFpMA). Coming soon: chat moderation, editing and deleting messages. + +Get started using the [📚 documentation](https://ably.com/docs/products/chat). + +![Ably Chat Header](/images/ably-chat-github-header.png) ## Supported Platforms @@ -17,11 +32,438 @@ Xcode 16 or later. ## Installation -The SDK is distributed as a Swift package and can hence be installed using Xcode or by adding it as a dependency in your package’s `Package.swift`. We’ll add detailed instructions when we release the first version of the SDK. +The SDK is distributed as a Swift package and can hence be installed using Xcode or by adding it as a dependency in your package’s `Package.swift`: + +```swift +.package(url: "https://github.com/ably/ably-chat-swift", from: "0.1.0") +``` + +## Supported chat features + +This project is under development so we will be incrementally adding new features. At this stage, you'll find APIs for the following chat +features: + +- Chat rooms for 1:1, 1:many, many:1 and many:many participation. +- Sending and receiving chat messages. +- Online status aka presence of chat participants. +- Chat room occupancy, i.e total number of connections and presence members. +- Typing indicators +- Room-level reactions (ephemeral at this stage) + +If there are other features you'd like us to prioritize, please [let us know](https://forms.gle/mBw9M53NYuCBLFpMA). + +## Usage + +You will need the following prerequisites: + +- An Ably account + - You can [sign up](https://ably.com/signup) to the generous free tier. + +- An Ably API key + - Use the default or create a new API key in an app within + your [Ably account dashboard](https://ably.com/dashboard). + - Make sure your API key has the + following [capabilities](https://ably.com/docs/auth/capabilities): `publish`, `subscribe`, `presence`, `history` and + `channel-metadata`. + +To instantiate the Chat SDK, create an [Ably client](https://ably.com/docs/getting-started/setup) and pass it into the Chat constructor: + +```swift +let realtimeOptions = ARTClientOptions() +realtimeOptions.key = "" +realtimeOptions.clientId = "" +let realtime = ARTRealtime(options: realtimeOptions) +let chatClient = DefaultChatClient(realtime: realtime, clientOptions: nil) +``` + +You can use [basic authentication](https://ably.com/docs/auth/basic) i.e. the API Key directly for testing purposes, +however it is strongly recommended that you use [token authentication](https://ably.com/docs/auth/token) in production +environments. + +To use Chat you must also set a [`clientId`](https://ably.com/docs/auth/identified-clients) so that users are +identifiable. + +## Connections + +The Chat SDK uses a single connection to Ably, which is exposed via the `ChatClient#connection` property. You can use this +property to observe the connection state and take action accordingly. + +### Current connection status + +You can view the current connection status at any time: + +```swift +let status = await chatClient.connection.status +let error = await chatClient.connection.error +``` + +### Subscribing to connection status changes + +You can subscribe to connection status changes by registering a listener, like so: + +```swift +let subscription = chatClient.connection.onStatusChange(bufferingPolicy: .unbounded) +for await statusChange in subscription { + print("Connection status changed to: \(statusChange.current)") +} +``` + +To stop listening to changes, call the `finish` method on the returned subscription instance: + +```swift +subscription.finish() +``` + +## Chat rooms + +### Creating or retrieving a chat room + +You can create or retrieve a chat room with name `"basketball-stream"` this way: + +```swift +let room = try await chatClient.rooms.get(roomID: "basketball-stream", options: RoomOptions()) +``` + +The second argument to `rooms.get` is a `RoomOptions` argument, which tells the Chat SDK what features you would like your room to use and +how they should be configured. + +You can also use `RoomOptions.allFeaturesEnabled` to enable all room features with the default configuration. + +For example, you can set the timeout between keystrokes for typing events as part of the room options. Sensible defaults for each of the +features are provided for your convenience: + +- A typing timeout (time of inactivity before typing stops) of 5 seconds. +- Entry and subscription to presence. + +Here’s an example demonstrating how to specify a custom typing timeout of 3 seconds: + +```swift +let room = try await chatClient.rooms.get(roomID: "basketball-stream", + options: .init(typing: TypingOptions(timeout: 3.0))) +``` + +In order to use the same room but with different options, you must first `release` the room before requesting an instance with the changed +options (see below for more information on releasing rooms). + +Note that: + +- If a `release` call is currently in progress for the room (see below), then a call to `get` will wait for that to resolve before resolving + itself. +- If a `get` call is currently in progress for the room and `release` is called, the `get` call will reject. + +### Attaching to a room + +To start receiving events on a room, it must first be attached. This can be done using the `attach` method: + +```swift +try await room.attach() +``` + +### Detaching from a room + +To stop receiving events on a room, it must be detached, which can be achieved by using the `detach` method: + +```swift +room.detach() +``` + +Note: This does not remove any event listeners you have registered and they will begin to receive events again in the +event that the room is re-attached. + +### Releasing a room + +Depending on your application, you may have multiple rooms that come and go over time (e.g. if you are running 1:1 support chat). When you +are completely finished with a room, you may `release` it which allows the underlying resources to be collected: + +```swift +_ = try await rooms.release(roomID: "basketball-stream") +``` + +Once `release` is called, the room will become unusable and you will need to get a new instance using `rooms.get`. + +> [!NOTE] +> Releasing a room may be optional for many applications. If release is not called, the server will automatically tidy up +connections and other resources associated with the room after a period of time. + +### Monitoring room status + +Monitoring the status of the room is key to a number of common chat features. For example, you might want to display a warning when the room +has become detached. + +### Current status of a room + +To get the current status, you can use the `status` property: + +```swift +let status = try await room.status +let error = try await status.error +``` + +### Listening to room status updates + +You can also subscribe to changes in the room status and be notified whenever they happen by registering a listener: + +```swift +let statusSubscription = try await room.onStatusChange(bufferingPolicy: .unbounded) +for await status in statusSubscription { + print("Room status: \(status)") +} +``` + +To stop listening to room status changes, call the `finish` method on the returned subscription instance: + +```swift +statusSubscription.finish() +``` + +## Handling discontinuity + +There may be instances where the connection to Ably is lost for a period of time, for example, when the user enters a tunnel. In many +circumstances, the connection will recover and operation will continue with no discontinuity of messages. However, during extended +periods of disconnection, continuity cannot be guaranteed and you'll need to take steps to recover messages you might have missed. + +Each feature of the Chat SDK provides an `subscribeToDiscontinuities` method. Here you can register a listener that will be notified whenever a +discontinuity in that feature has been observed. + +Taking messages as an example, you can listen for discontinuities like so: + +```swift +let subscription = room.messages.subscribeToDiscontinuities() +for await error in subscription { + print("Recovering from the error: \(error)") +} +``` + +To stop listening to discontinuities, call `finish` method on returned subscription instance. + +## Chat messages + +### Subscribing to incoming messages + +To subscribe to incoming messages you create a subscription for the room `messages` object: + +```swift +let messagesSubscription = try await room.messages.subscribe(bufferingPolicy: .unbounded) +for await message in messagesSubscription { + print("Message received: \(message)") +} +``` + +To stop listening for the new messages, call the `finish` method on the returned subscription instance. + +### Sending messages + +To send a message, simply call `send` on the room `messages` property, with the message you want to send: + +```swift +let message = try await room.messages.send(params: .init(text: "hello")) +``` + +### Retrieving message history + +The messages object also exposes the `get` method which can be used to request historical messages in the chat room according +to the given criteria. It returns a paginated response that can be used to request more messages: + +```swift +let paginatedResult = try await room.messages.get(options: .init(orderBy: .newestFirst)) +print(paginatedResult.items) + +if paginatedResult.hasNext { + let next = try await paginatedResult.next! + print(next.items) +} else { + print("End of messages") +} +``` + +### Retrieving message history for a subscribed listener + +In addition to being able to unsubscribe from messages, the return value from `messages.subscribe` also includes the `getPreviousMessages` +method. It can be used to request historical messages in the chat room that were sent up to the point that a particular listener was subscribed. It returns a +paginated response that can be used to request for more messages: + +```swift +let messagesSubscription = try await room.messages.subscribe(bufferingPolicy: .unbounded) +let paginatedResult = try await messagesSubscription.getPreviousMessages(params: .init(limit: 50)) // `orderBy` here is ignored and always `newestFirst` +print(paginatedResult.items) + +if paginatedResult.hasNext { + let next = try await paginatedResult.next! + print(next.items) +} else { + print("End of messages") +} +``` + +## Online status + +### Retrieving online members + +You can get the complete list of currently online or present members, their state and data, by calling the `presence#get` method which returns +a list of the presence messages, where each message contains the most recent data for a member: + +```swift +// Retrieve all users entered into presence as an array: +let presentMembers = try await room.presence.get() + +// Retrieve the status of specific users by their clientId: +let presentMember = try await room.presence.get(params: .init(clientID: "clemons123")) + +// To check whether the user is online or not: +let isPresent = try await room.presence.isUserPresent(clientID: "clemons123") +``` + +### Entering the presence set + +To appear online for other users, you can enter the presence set of a chat room. While entering presence, you can provide optional data that +will be associated with the presence message: + +```swift +try await room.presence.enter(data: .init(userCustomData: ["status": .string("Online")])) +``` + +### Updating the presence data + +Updates allow you to make changes to the custom data associated with a present user. Common use-cases include updating the users' +status: + +```swift +try await room.presence.update(data: .init(userCustomData: ["status": .string("Busy")])) +``` + +### Leaving the presence set + +Ably automatically triggers a presence leave if a client goes offline. But you can also manually leave the presence set as a result of a UI +action. While leaving presence, you can provide optional data that will be associated with the presence message: + +```swift +try await room.presence.leave(data: .init(userCustomData: ["status": .string("Bye!")])) +``` + +### Subscribing to presence updates + +You can provide a single listener for all presence event types: + +```swift +let presenceSubscription = try await room.presence.subscribe(events: [.enter, .leave, .update]) +for await event in presenceSubscription { + print("Presence event `\(event.action)` from `\(event.clientId)` with data `\(event.data)`") +} +``` + +To stop listening for the presence updates, call the `finish` method on the returned subscription instance. + +## Typing indicators + +> [!NOTE] +> You should be attached to the room to enable this functionality. + +Typing events allow you to inform others that a client is typing and also subscribe to others' typing status. + +### Retrieving the set of current typers + +You can get the complete set of the current typing `clientId`s, by calling the `typing.get` method. + +```swift +// Retrieve the entire list of currently typing clients +let currentlyTypingClientIds = try await room.typing.get() +``` + +### Start typing + +To inform other users that you are typing, you can call the start method. This will begin a timer that will automatically stop typing after +a set amount of time. + +```swift +try await room.typing.start() +``` + +Repeated calls to start will reset the timer, so the clients typing status will remain active. + +### Stop typing + +You can immediately stop typing without waiting for the timer to expire. + +```swift +try await room.typing.start() +// Some short delay - timer not yet expired + +try await room.typing.stop() +// Timer cleared and stopped typing event emitted and listeners are notified +``` + +### Subscribing to typing updates + +To subscribe to typing events, create a subscription with the `subscribe` method: + +```swift +let typingSubscription = try await room.typing.subscribe(bufferingPolicy: .unbounded) +for await typing in typingSubscription { + typingInfo = typing.currentlyTyping.isEmpty ? "" : "Typing: \(typing.currentlyTyping.joined(separator: ", "))..." +} +``` +To stop listening for the typing events, call the `finish` method on the returned subscription instance. + +## Occupancy of a chat room + +Occupancy tells you how many users are connected to the chat room. + +### Subscribing to occupancy updates + +To subscribe to occupancy updates, subscribe a listener to the chat room `occupancy` member: + +```swift +let occupancySubscription = try await room.occupancy.subscribe(bufferingPolicy: .unbounded) +for await event in occupancySubscription { + occupancyInfo = "Connections: \(event.presenceMembers) (\(event.connections))" +} +``` +To stop listening for the typing events, call the `finish` method on the returned subscription instance. + +Occupancy updates are delivered in near-real-time, with updates in quick succession batched together for performance. + +### Retrieving the occupancy of a chat room + +You can request the current occupancy of a chat room using the `occupancy.get` method: + +```swift +let occupancy = try await room.occupancy.get() +``` + +## Room-level reactions + +You can subscribe to and send ephemeral room-level reactions by using the room `reactions` object. +To send room-level reactions, you must be [attached](#attaching-to-a-room) to the room. + +### Sending a reaction + +To send a reaction such as `"like"`: + +```swift +try await room.reactions.send(params: .init(type: "like")) +``` + +You can also add any metadata and headers to reactions: + +```swift +try await room.reactions.send(params: .init(type: "🎉", metadata: ["effect": .string("fireworks")])) +``` + +### Subscribing to room reactions + +Subscribe to receive room-level reactions: + +```swift +let reactionSubscription = try await room.reactions.subscribe(bufferingPolicy: .unbounded) +for await reaction in reactionSubscription { + print("Received a reaction of type \(reaction.type), and metadata \(reaction.metadata)") +} +``` +To stop receiving reactions, call the `finish` method on the returned subscription instance. ## Example app -This repository contains an example app, written using SwiftUI, which demonstrates how to use the SDK. The code for this app is in the [`Example` directory](Example). +This repository contains an example app, written using SwiftUI, which demonstrates how to use the SDK. The code for this app is in the [`Example`](Example) directory. In order to allow the app to use modern SwiftUI features, it supports the following OS versions: @@ -31,6 +473,49 @@ In order to allow the app to use modern SwiftUI features, it supports the follow To run the app, open the `AblyChat.xcworkspace` workspace in Xcode and run the `AblyChatExample` target. If you wish to run it on an iOS or tvOS device, you’ll need to set up code signing. +## In-depth + +### Channels Behind Chat Features + +It might be useful to know that each feature is backed by an underlying Pub/Sub channel. You can use this information to enable +interoperability with other platforms by subscribing to the channels directly using +the [Ably Pub/Sub SDKs](https://ably.com/docs/products/channels) for those platforms. + +The channel for each feature can be obtained via the `channel` property +on that feature. + +```swift +let messagesChannel = room.messages.channel +``` + +**Warning**: You should not attempt to change the state of a channel directly. Doing so may cause unintended side-effects in the Chat SDK. + +### Channels Used + +For a given chat room, the channels used for features are as follows: + +| Feature | Channel | +|-----------|--------------------------------------| +| Messages | `::$chat::$chatMessages` | +| Presence | `::$chat::$chatMessages` | +| Occupancy | `::$chat::$chatMessages` | +| Reactions | `::$chat::$reactions` | +| Typing | `::$chat::$typingIndicators` | + +--- + ## Contributing -For information on how to contribute to this repository, please see the [contributing guidelines](CONTRIBUTING.md). +For guidance on how to contribute to this project, see the [contributing guidelines](CONTRIBUTING.md). + +## Support, feedback and troubleshooting + +Please visit http://support.ably.com/ for access to our knowledge base and to ask for any assistance. You can also view +the community reported [Github issues](https://github.com/ably/ably-chat-swift/issues) or raise one yourself. + +To see what has changed in recent versions, see the [changelog](CHANGELOG.md). + +## Further reading + +- [Sign up](https://forms.gle/gRZa51erqNp1mSxVA) to the private beta and get started. +- [Share feedback or request](https://forms.gle/mBw9M53NYuCBLFpMA) a new feature.