From 8c069495fa3f019da25eb24edab281da924ffd2b Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 20 Aug 2024 21:44:30 +0100 Subject: [PATCH 1/2] [ECO-4899] feat: add public API for Chat SDK Based on Ably Chat JS SDK repo at 0b4b0f8. This is just a first attempt and all the decisions can be revisited. Notable TypeScript-to-Kotlin decisions: - Functions that return Promises become suspend functions. - I decided to leave listeners as they are and not use `Flow` in the public interface. These listeners will be invoked synchronously with the events they are listening to. This approach makes it easy to provide extension functions for an idiomatic `Flow` interface on one hand, and on the other hand, it gives maximum flexibility for SDK users to utilize other reactive libraries like Reactor or RxJava. - The presence data type is `Any` for now. --- .../src/main/java/com/ably/chat/ChatClient.kt | 37 ++++ .../main/java/com/ably/chat/ClientOptions.kt | 20 ++ .../src/main/java/com/ably/chat/Connection.kt | 11 ++ .../java/com/ably/chat/ConnectionStatus.kt | 107 +++++++++++ .../com/ably/chat/EmitsDiscontinuities.kt | 31 ++++ .../src/main/java/com/ably/chat/ErrorCodes.kt | 90 +++++++++ .../src/main/java/com/ably/chat/EventTypes.kt | 50 +++++ .../src/main/java/com/ably/chat/Headers.kt | 19 ++ .../src/main/java/com/ably/chat/LogLevel.kt | 40 ++++ .../src/main/java/com/ably/chat/Message.kt | 70 +++++++ .../src/main/java/com/ably/chat/Messages.kt | 173 ++++++++++++++++++ .../src/main/java/com/ably/chat/Metadata.kt | 16 ++ .../src/main/java/com/ably/chat/Occupancy.kt | 66 +++++++ .../src/main/java/com/ably/chat/Presence.kt | 134 ++++++++++++++ .../src/main/java/com/ably/chat/Reaction.kt | 46 +++++ .../src/main/java/com/ably/chat/Room.kt | 82 +++++++++ .../main/java/com/ably/chat/RoomOptions.kt | 74 ++++++++ .../main/java/com/ably/chat/RoomReactions.kt | 102 +++++++++++ .../src/main/java/com/ably/chat/RoomStatus.kt | 112 ++++++++++++ .../src/main/java/com/ably/chat/Typing.kt | 79 ++++++++ detekt.yml | 4 +- 21 files changed, 1361 insertions(+), 2 deletions(-) create mode 100644 chat-android/src/main/java/com/ably/chat/ChatClient.kt create mode 100644 chat-android/src/main/java/com/ably/chat/ClientOptions.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Connection.kt create mode 100644 chat-android/src/main/java/com/ably/chat/ConnectionStatus.kt create mode 100644 chat-android/src/main/java/com/ably/chat/EmitsDiscontinuities.kt create mode 100644 chat-android/src/main/java/com/ably/chat/ErrorCodes.kt create mode 100644 chat-android/src/main/java/com/ably/chat/EventTypes.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Headers.kt create mode 100644 chat-android/src/main/java/com/ably/chat/LogLevel.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Message.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Messages.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Metadata.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Occupancy.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Presence.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Reaction.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Room.kt create mode 100644 chat-android/src/main/java/com/ably/chat/RoomOptions.kt create mode 100644 chat-android/src/main/java/com/ably/chat/RoomReactions.kt create mode 100644 chat-android/src/main/java/com/ably/chat/RoomStatus.kt create mode 100644 chat-android/src/main/java/com/ably/chat/Typing.kt diff --git a/chat-android/src/main/java/com/ably/chat/ChatClient.kt b/chat-android/src/main/java/com/ably/chat/ChatClient.kt new file mode 100644 index 00000000..e0434110 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/ChatClient.kt @@ -0,0 +1,37 @@ +package com.ably.chat + +import io.ably.lib.realtime.AblyRealtime +import io.ably.lib.types.ClientOptions + +typealias RealtimeClient = AblyRealtime + +/** + * This is the core client for Ably chat. It provides access to chat rooms. + */ +interface ChatClient { + /** + * The rooms object, which provides access to chat rooms. + */ + val room: Room + + /** + * The underlying connection to Ably, which can be used to monitor the clients + * connection to Ably servers. + */ + val connection: Connection + + /** + * The clientId of the current client. + */ + val clientId: String + + /** + * The underlying Ably Realtime client. + */ + val realtime: RealtimeClient + + /** + * The resolved client options for the client, including any defaults that have been set. + */ + val clientOptions: ClientOptions +} diff --git a/chat-android/src/main/java/com/ably/chat/ClientOptions.kt b/chat-android/src/main/java/com/ably/chat/ClientOptions.kt new file mode 100644 index 00000000..adc33120 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/ClientOptions.kt @@ -0,0 +1,20 @@ +package com.ably.chat + +import io.ably.lib.util.Log.LogHandler + +/** + * Configuration options for the chat client. + */ +data class ClientOptions( + /** + * A custom log handler that will be used to log messages from the client. + * @defaultValue The client will log messages to the console. + */ + val logHandler: LogHandler? = null, + + /** + * The minimum log level at which messages will be logged. + * @defaultValue LogLevel.Error + */ + val logLevel: LogLevel = LogLevel.Error, +) diff --git a/chat-android/src/main/java/com/ably/chat/Connection.kt b/chat-android/src/main/java/com/ably/chat/Connection.kt new file mode 100644 index 00000000..a1378cce --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Connection.kt @@ -0,0 +1,11 @@ +package com.ably.chat + +/** + * Represents a connection to Ably. + */ +interface Connection { + /** + * The current status of the connection. + */ + val status: ConnectionStatus +} diff --git a/chat-android/src/main/java/com/ably/chat/ConnectionStatus.kt b/chat-android/src/main/java/com/ably/chat/ConnectionStatus.kt new file mode 100644 index 00000000..6858f507 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/ConnectionStatus.kt @@ -0,0 +1,107 @@ +package com.ably.chat + +import io.ably.lib.types.ErrorInfo + +/** + * Default timeout for transient states before we attempt handle them as a state change. + */ +const val TRANSIENT_TIMEOUT = 5000 + +/** + * Represents a connection to Ably. + */ +interface ConnectionStatus { + /** + * The current status of the connection. + */ + val current: ConnectionLifecycle + + /** + * The current error, if any, that caused the connection to enter the current status. + */ + val error: ErrorInfo? + + /** + * Registers a listener that will be called whenever the connection status changes. + * @param listener The function to call when the status changes. + */ + fun on(listener: Listener) + + /** + * Unregisters a listener + * @param listener The function to call when the status changes. + */ + fun off(listener: Listener) + + /** + * An interface for listening to changes for the connection status + */ + fun interface Listener { + /** + * A function that can be called when the connection status changes. + * @param change The change in status. + */ + fun connectionStatusChanged(change: ConnectionStatusChange) + } +} + +/** + * The different states that the connection can be in through its lifecycle. + */ +enum class ConnectionLifecycle(val stateName: String) { + /** + * A temporary state for when the library is first initialized. + */ + Initialized("initialized"), + + /** + * The library is currently connecting to Ably. + */ + Connecting("connecting"), + + /** + * The library is currently connected to Ably. + */ + Connected("connected"), + + /** + * The library is currently disconnected from Ably, but will attempt to reconnect. + */ + Disconnected("disconnected"), + + /** + * The library is in an extended state of disconnection, but will attempt to reconnect. + */ + Suspended("suspended"), + + /** + * The library is currently disconnected from Ably and will not attempt to reconnect. + */ + Failed("failed"), +} + +/** + * Represents a change in the status of the connection. + */ +data class ConnectionStatusChange( + /** + * The new status of the connection. + */ + val current: ConnectionLifecycle, + + /** + * The previous status of the connection. + */ + val previous: ConnectionLifecycle, + + /** + * An error that provides a reason why the connection has + * entered the new status, if applicable. + */ + val error: ErrorInfo?, + + /** + * The time in milliseconds that the client will wait before attempting to reconnect. + */ + val retryIn: Long?, +) diff --git a/chat-android/src/main/java/com/ably/chat/EmitsDiscontinuities.kt b/chat-android/src/main/java/com/ably/chat/EmitsDiscontinuities.kt new file mode 100644 index 00000000..10ffac63 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/EmitsDiscontinuities.kt @@ -0,0 +1,31 @@ +package com.ably.chat + +import io.ably.lib.types.ErrorInfo + +/** + * An interface to be implemented by objects that can emit discontinuities to listeners. + */ +interface EmitsDiscontinuities { + /** + * Register a listener to be called when a discontinuity is detected. + * @param listener The listener to be called when a discontinuity is detected. + */ + fun onDiscontinuity(listener: Listener) + + /** + * Unregister a listener to be called when a discontinuity is detected. + * @param listener The listener + */ + fun offDiscontinuity(listener: Listener) + + /** + * An interface for listening when discontinuity happens + */ + fun interface Listener { + /** + * A function that can be called when discontinuity happens. + * @param reason reason for discontinuity + */ + fun discontinuityEmitted(reason: ErrorInfo?) + } +} diff --git a/chat-android/src/main/java/com/ably/chat/ErrorCodes.kt b/chat-android/src/main/java/com/ably/chat/ErrorCodes.kt new file mode 100644 index 00000000..39b5f130 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/ErrorCodes.kt @@ -0,0 +1,90 @@ +package com.ably.chat + +/** + * Error codes for the Chat SDK. + */ +object ErrorCodes { + /** + * The messages feature failed to attach. + */ + const val MessagesAttachmentFailed = 102_001 + + /** + * The presence feature failed to attach. + */ + const val PresenceAttachmentFailed = 102_002 + + /** + * The reactions feature failed to attach. + */ + const val ReactionsAttachmentFailed = 102_003 + + /** + * The occupancy feature failed to attach. + */ + const val OccupancyAttachmentFailed = 102_004 + + /** + * The typing feature failed to attach. + */ + const val TypingAttachmentFailed = 102_005 + // 102006 - 102049 reserved for future use for attachment errors + + /** + * The messages feature failed to detach. + */ + const val MessagesDetachmentFailed = 102_050 + + /** + * The presence feature failed to detach. + */ + const val PresenceDetachmentFailed = 102_051 + + /** + * The reactions feature failed to detach. + */ + const val ReactionsDetachmentFailed = 102_052 + + /** + * The occupancy feature failed to detach. + */ + const val OccupancyDetachmentFailed = 102_053 + + /** + * The typing feature failed to detach. + */ + const val TypingDetachmentFailed = 102_054 + // 102055 - 102099 reserved for future use for detachment errors + + /** + * The room has experienced a discontinuity. + */ + const val RoomDiscontinuity = 102_100 + + // Unable to perform operation; + + /** + * Cannot perform operation because the room is in a failed state. + */ + const val RoomInFailedState = 102_101 + + /** + * Cannot perform operation because the room is in a releasing state. + */ + const val RoomIsReleasing = 102_102 + + /** + * Cannot perform operation because the room is in a released state. + */ + const val RoomIsReleased = 102_103 + + /** + * Cannot perform operation because the previous operation failed. + */ + const val PreviousOperationFailed = 102_104 + + /** + * An unknown error has happened in the room lifecycle. + */ + const val RoomLifecycleError = 102_105 +} diff --git a/chat-android/src/main/java/com/ably/chat/EventTypes.kt b/chat-android/src/main/java/com/ably/chat/EventTypes.kt new file mode 100644 index 00000000..36603760 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/EventTypes.kt @@ -0,0 +1,50 @@ +package com.ably.chat + +/** + * All chat message events. + */ +enum class MessageEventType(val eventName: String) { + /** Fires when a new chat message is received. */ + Created("message.created"), +} + +/** + * Enum representing presence events. + */ +enum class PresenceEventType(val eventName: String) { + /** + * Event triggered when a user enters. + */ + Enter("enter"), + + /** + * Event triggered when a user leaves. + */ + Leave("leave"), + + /** + * Event triggered when a user updates their presence data. + */ + Update("update"), + + /** + * Event triggered when a user initially subscribes to presence. + */ + Present("present"), +} + +enum class TypingEventType(val eventName: String) { + /** The set of currently typing users has changed. */ + Changed("typing.changed"), +} + +/** + * Room reaction events. This is used for the realtime system since room reactions + * have only one event: "roomReaction". + */ +enum class RoomReactionEventType(val eventName: String) { + /** + * Event triggered when a room reaction was received. + */ + Reaction("roomReaction"), +} diff --git a/chat-android/src/main/java/com/ably/chat/Headers.kt b/chat-android/src/main/java/com/ably/chat/Headers.kt new file mode 100644 index 00000000..7210e563 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Headers.kt @@ -0,0 +1,19 @@ +package com.ably.chat + +/** + * Headers are a flat key-value map that can be attached to chat messages. + * + * The headers are a flat key-value map and are sent as part of the realtime + * message's extras inside the `headers` property. They can serve similar + * purposes as Metadata but as opposed to Metadata they are read by Ably and + * can be used for features such as + * [subscription filters](https://faqs.ably.com/subscription-filters). + * + * Do not use the headers for authoritative information. There is no + * server-side validation. When reading the headers treat them like user + * input. + * + * The key prefix `ably-chat` is reserved and cannot be used. Ably may add + * headers prefixed with `ably-chat` in the future. + */ +typealias Headers = Map diff --git a/chat-android/src/main/java/com/ably/chat/LogLevel.kt b/chat-android/src/main/java/com/ably/chat/LogLevel.kt new file mode 100644 index 00000000..cc01da20 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/LogLevel.kt @@ -0,0 +1,40 @@ +package com.ably.chat + +/** + * Represents the different levels of logging that can be used. + */ +enum class LogLevel(val logLevelValue: Int) { + /** + * Something routine and expected has occurred. This level will provide logs for the vast majority of operations + * and function calls. + */ + Trace(0), + + /** + * Development information, messages that are useful when trying to debug library behavior, + * but superfluous to normal operation. + */ + Debug(1), + + /** + * Informational messages. Operationally significant to the library but not out of the ordinary. + */ + Info(2), + + /** + * Anything that is not immediately an error, but could cause unexpected behavior in the future. For example, + * passing an invalid value to an option. Indicates that some action should be taken to prevent future errors. + */ + Warn(3), + + /** + * A given operation has failed and cannot be automatically recovered. The error may threaten the continuity + * of operation. + */ + Error(4), + + /** + * No logging will be performed. + */ + Silent(5), +} diff --git a/chat-android/src/main/java/com/ably/chat/Message.kt b/chat-android/src/main/java/com/ably/chat/Message.kt new file mode 100644 index 00000000..6edee6ac --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Message.kt @@ -0,0 +1,70 @@ +package com.ably.chat + +/** + * {@link Headers} type for chat messages. + */ +typealias MessageHeaders = Headers + +/** + * {@link Metadata} type for chat messages. + */ +typealias MessageMetadata = Metadata + +/** + * Represents a single message in a chat room. + */ +data class Message( + /** + * The unique identifier of the message. + */ + val timeserial: String, + + /** + * The clientId of the user who created the message. + */ + val clientId: String, + + /** + * The roomId of the chat room to which the message belongs. + */ + val roomId: String, + + /** + * The text of the message. + */ + val textval: String, + + /** + * The timestamp at which the message was created. + */ + val createdAt: Long, + + /** + * The metadata of a chat message. Allows for attaching extra info to a message, + * which can be used for various features such as animations, effects, or simply + * to link it to other resources such as images, relative points in time, etc. + * + * Metadata is part of the Ably Pub/sub message content and is not read by Ably. + * + * This value is always set. If there is no metadata, this is an empty object. + * + * Do not use metadata for authoritative information. There is no server-side + * validation. When reading the metadata treat it like user input. + */ + val metadata: MessageMetadata, + + /** + * The headers of a chat message. Headers enable attaching extra info to a message, + * which can be used for various features such as linking to a relative point in + * time of a livestream video or flagging this message as important or pinned. + * + * Headers are part of the Ably realtime message extras.headers and they can be used + * for Filtered Subscriptions and similar. + * + * This value is always set. If there are no headers, this is an empty object. + * + * Do not use the headers for authoritative information. There is no server-side + * validation. When reading the headers treat them like user input. + */ + val headersval: MessageHeaders, +) diff --git a/chat-android/src/main/java/com/ably/chat/Messages.kt b/chat-android/src/main/java/com/ably/chat/Messages.kt new file mode 100644 index 00000000..37f12409 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Messages.kt @@ -0,0 +1,173 @@ +package com.ably.chat + +import io.ably.lib.realtime.Channel +import io.ably.lib.types.PaginatedResult + +/** + * This interface is used to interact with messages in a chat room: subscribing + * to new messages, fetching history, or sending messages. + * + * Get an instance via {@link Room.messages}. + */ +interface Messages : EmitsDiscontinuities { + /** + * Get the underlying Ably realtime channel used for the messages in this chat room. + * + * @returns the realtime channel + */ + val channel: Channel + + /** + * Subscribe to new messages in this chat room. + * @param listener callback that will be called + * @returns A response object that allows you to control the subscription. + */ + fun subscribe(listener: Listener) + + /** + * Unsubscribe listener + * @param listener callback that will be unsubscribed + */ + fun unsubscribe(listener: Listener) + + /** + * Get messages that have been previously sent to the chat room, based on the provided options. + * + * @param options Options for the query. + * @returns A promise that resolves with the paginated result of messages. This paginated result can + * be used to fetch more messages if available. + */ + suspend fun get(options: QueryOptions): PaginatedResult + + /** + * Send a message in the chat room. + * + * This method uses the Ably Chat API endpoint for sending messages. + * + * Note that the Promise may resolve before OR after the message is received + * from the realtime channel. This means you may see the message that was just + * sent in a callback to `subscribe` before the returned promise resolves. + * + * @param params an object containing {text, headers, metadata} for the message + * to be sent. Text is required, metadata and headers are optional. + * @returns A promise that resolves when the message was published. + */ + fun send(params: SendMessageParams): Message + + /** + * An interface for listening to new messaging event + */ + fun interface Listener { + /** + * A function that can be called when the new messaging event happens. + * @param event The event that happened. + */ + fun onEvent(event: MessageEvent) + } +} + +/** + * Options for querying messages in a chat room. + */ +data class QueryOptions( + /** + * The start of the time window to query from. If provided, the response will include + * messages with timestamps equal to or greater than this value. + * + * @defaultValue The beginning of time + */ + val start: Long? = null, + + /** + * The end of the time window to query from. If provided, the response will include + * messages with timestamps less than this value. + * + * @defaultValue Now + */ + val end: Long? = null, + + /** + * The maximum number of messages to return in the response. + */ + val limit: Int = 100, + + /** + * The direction to query messages in. + */ + val direction: Direction = Direction.FORWARDS, +) { + /** + * Represents direction to query messages in. + */ + enum class Direction { + /** + * The response will include messages from the start of the time window to the end. + */ + FORWARDS, + + /** + * the response will include messages from the end of the time window to the start. + */ + BACKWARDS, + } +} + +/** + * Payload for a message event. + */ +data class MessageEvent( + /** + * The type of the message event. + */ + val type: MessageEventType, + + /** + * The message that was received. + */ + val message: Message, +) + +/** + * Params for sending a text message. Only `text` is mandatory. + */ +data class SendMessageParams( + /** + * The text of the message. + */ + val text: String, + + /** + * Optional metadata of the message. + * + * The metadata is a map of extra information that can be attached to chat + * messages. It is not used by Ably and is sent as part of the realtime + * message payload. Example use cases are setting custom styling like + * background or text colors or fonts, adding links to external images, + * emojis, etc. + * + * Do not use metadata for authoritative information. There is no server-side + * validation. When reading the metadata treat it like user input. + * + * The key `ably-chat` is reserved and cannot be used. Ably may populate + * this with different values in the future. + */ + val metadata: MessageMetadata? = null, + + /** + * Optional headers of the message. + * + * The headers are a flat key-value map and are sent as part of the realtime + * message's extras inside the `headers` property. They can serve similar + * purposes as the metadata but they are read by Ably and can be used for + * features such as + * [subscription filters](https://faqs.ably.com/subscription-filters). + * + * Do not use the headers for authoritative information. There is no + * server-side validation. When reading the headers treat them like user + * input. + * + * The key prefix `ably-chat` is reserved and cannot be used. Ably may add + * headers prefixed with `ably-chat` in the future. + */ + val headers: MessageHeaders? = null, +) diff --git a/chat-android/src/main/java/com/ably/chat/Metadata.kt b/chat-android/src/main/java/com/ably/chat/Metadata.kt new file mode 100644 index 00000000..1135fb1a --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Metadata.kt @@ -0,0 +1,16 @@ +package com.ably.chat + +/** + * Metadata is a map of extra information that can be attached to chat + * messages. It is not used by Ably and is sent as part of the realtime + * message payload. Example use cases are setting custom styling like + * background or text colors or fonts, adding links to external images, + * emojis, etc. + * + * Do not use metadata for authoritative information. There is no server-side + * validation. When reading the metadata treat it like user input. + * + * The key `ably-chat` is reserved and cannot be used. Ably may populate + * this with different values in the future. + */ +typealias Metadata = Map diff --git a/chat-android/src/main/java/com/ably/chat/Occupancy.kt b/chat-android/src/main/java/com/ably/chat/Occupancy.kt new file mode 100644 index 00000000..60013529 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Occupancy.kt @@ -0,0 +1,66 @@ +package com.ably.chat + +import io.ably.lib.realtime.Channel + +/** + * This interface is used to interact with occupancy in a chat room: subscribing to occupancy updates and + * fetching the current room occupancy metrics. + * + * Get an instance via {@link Room.occupancy}. + */ +interface Occupancy : EmitsDiscontinuities { + /** + * Get underlying Ably channel for occupancy events. + * + * @returns The underlying Ably channel for occupancy events. + */ + val channel: Channel + + /** + * Subscribe a given listener to occupancy updates of the chat room. + * + * @param listener A listener to be called when the occupancy of the room changes. + * @returns A promise resolves to the channel attachment state change event from the implicit channel attach operation. + */ + fun subscribe(listener: Listener) + + /** + * Unsubscribe a given listener to occupancy updates of the chat room. + * + * @param listener A listener to be unsubscribed. + */ + fun unsubscribe(listener: Listener) + + /** + * Get the current occupancy of the chat room. + * + * @returns A promise that resolves to the current occupancy of the chat room. + */ + suspend fun get(): OccupancyEvent + + /** + * An interface for listening to new occupancy event + */ + fun interface Listener { + /** + * A function that can be called when the new occupancy event happens. + * @param event The event that happened. + */ + fun onEvent(event: OccupancyEvent) + } +} + +/** + * Represents the occupancy of a chat room. + */ +data class OccupancyEvent( + /** + * The number of connections to the chat room. + */ + val connections: Int, + + /** + * The number of presence members in the chat room - members who have entered presence. + */ + val presenceMembers: Int, +) diff --git a/chat-android/src/main/java/com/ably/chat/Presence.kt b/chat-android/src/main/java/com/ably/chat/Presence.kt new file mode 100644 index 00000000..c2c55b1f --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Presence.kt @@ -0,0 +1,134 @@ +package com.ably.chat + +import android.text.PrecomputedText.Params +import io.ably.lib.realtime.Channel +import io.ably.lib.types.PresenceMessage + +typealias PresenceData = Any + +/** + * This interface is used to interact with presence in a chat room: subscribing to presence events, + * fetching presence members, or sending presence events (join,update,leave). + * + * Get an instance via {@link Room.presence}. + */ +interface Presence : EmitsDiscontinuities { + /** + * Get the underlying Ably realtime channel used for presence in this chat room. + * @returns The realtime channel. + */ + val channel: Channel + + /** + * Method to get list of the current online users and returns the latest presence messages associated to it. + * @param {Ably.RealtimePresenceParams} params - Parameters that control how the presence set is retrieved. + * @returns {Promise} or upon failure, the promise will be rejected with an [[Ably.ErrorInfo]] object which explains the error. + */ + suspend fun get(params: List): List + + /** + * Method to check if user with supplied clientId is online + * @param {string} clientId - The client ID to check if it is present in the room. + * @returns true if user with specified clientId is present, false otherwise + */ + suspend fun isUserPresent(clientId: String): Boolean + + /** + * Method to join room presence, will emit an enter event to all subscribers. Repeat calls will trigger more enter events. + * @param {PresenceData} data - The users data, a JSON serializable object that will be sent to all subscribers. + * @returns {Promise} or upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error. + */ + suspend fun enter(data: PresenceData?) + + /** + * Method to update room presence, will emit an update event to all subscribers. If the user is not present, it will be treated as a join event. + * @param {PresenceData} data - The users data, a JSON serializable object that will be sent to all subscribers. + * @returns {Promise} or upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error. + */ + suspend fun update(data: PresenceData?) + + /** + * Method to leave room presence, will emit a leave event to all subscribers. If the user is not present, it will be treated as a no-op. + * @param {PresenceData} data - The users data, a JSON serializable object that will be sent to all subscribers. + * @returns {Promise} or upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error. + */ + suspend fun leave(data: PresenceData?) + + /** + * Subscribe the given listener to all presence events. + * @param listener listener to subscribe + */ + fun subscribe(listener: Listener) + + /** + * Unsubscribe the given listener to all presence events. + * @param listener listener to unsubscribe + */ + fun unsubscribe(listener: Listener) + + /** + * An interface for listening to new presence event + */ + fun interface Listener { + /** + * A function that can be called when the new presence event happens. + * @param event The event that happened. + */ + fun onEvent(event: PresenceEvent) + } +} + +/** + * Type for PresenceMember + */ +data class PresenceMember( + /** + * The clientId of the presence member. + */ + val clientId: String, + + /** + * The data associated with the presence member. + */ + val data: PresenceData, + + /** + * The current state of the presence member. + */ + val action: PresenceMessage.Action, + + /** + * The timestamp of when the last change in state occurred for this presence member. + */ + val updatedAt: Long, + + /** + * The extras associated with the presence member. + */ + val extras: Map? = null, +) + +/** + * Type for PresenceEvent + */ +data class PresenceEvent( + /** + * The type of the presence event. + */ + val action: PresenceMessage.Action, + + /** + * The clientId of the client that triggered the presence event. + */ + val clientId: String, + + /** + * The timestamp of the presence event. + */ + val timestamp: Int, + + /** + * The data associated with the presence event. + */ + val data: PresenceData, +) diff --git a/chat-android/src/main/java/com/ably/chat/Reaction.kt b/chat-android/src/main/java/com/ably/chat/Reaction.kt new file mode 100644 index 00000000..8ab083d5 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Reaction.kt @@ -0,0 +1,46 @@ +package com.ably.chat + +/** + * {@link Headers} type for chat messages. + */ +typealias ReactionHeaders = Headers + +/** + * {@link Metadata} type for chat messages. + */ +typealias ReactionMetadata = Metadata + +/** + * Represents a room-level reaction. + */ +data class Reaction( + /** + * The type of the reaction, for example "like" or "love". + */ + val type: String, + + /** + * Metadata of the reaction. If no metadata was set this is an empty object. + */ + val metadata: ReactionMetadata = mapOf(), + + /** + * Headers of the reaction. If no headers were set this is an empty object. + */ + val headers: ReactionHeaders = mapOf(), + + /** + * The timestamp at which the reaction was sent. + */ + val createdAt: Long, + + /** + * The clientId of the user who sent the reaction. + */ + val clientId: String, + + /** + * Whether the reaction was sent by the current user. + */ + val isSelf: Boolean, +) diff --git a/chat-android/src/main/java/com/ably/chat/Room.kt b/chat-android/src/main/java/com/ably/chat/Room.kt new file mode 100644 index 00000000..3f6616e5 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Room.kt @@ -0,0 +1,82 @@ +package com.ably.chat + +/** + * Represents a chat room. + */ +interface Room { + /** + * The unique identifier of the room. + * @returns The room identifier. + */ + val roomId: String + + /** + * Allows you to send, subscribe-to and query messages in the room. + * + * @returns The messages instance for the room. + */ + val messages: Messages + + /** + * Allows you to subscribe to presence events in the room. + * + * @throws {@link ErrorInfo}} if presence is not enabled for the room. + * @returns The presence instance for the room. + */ + val presence: Presence + + /** + * Allows you to interact with room-level reactions. + * + * @throws {@link ErrorInfo} if reactions are not enabled for the room. + * @returns The room reactions instance for the room. + */ + val reactions: RoomReactions + + /** + * Allows you to interact with typing events in the room. + * + * @throws {@link ErrorInfo} if typing is not enabled for the room. + * @returns The typing instance for the room. + */ + val typing: Typing + + /** + * Allows you to interact with occupancy metrics for the room. + * + * @throws {@link ErrorInfo} if occupancy is not enabled for the room. + * @returns The occupancy instance for the room. + */ + val occupancy: Occupancy + + /** + * Returns an object that can be used to observe the status of the room. + * + * @returns The status observable. + */ + val status: RoomStatus + + /** + * Returns the room options. + * + * @returns A copy of the options used to create the room. + */ + val options: RoomOptions + + /** + * Attaches to the room to receive events in realtime. + * + * If a room fails to attach, it will enter either the {@link RoomLifecycle.Suspended} or {@link RoomLifecycle.Failed} state. + * + * If the room enters the failed state, then it will not automatically retry attaching and intervention is required. + * + * If the room enters the suspended state, then the call to attach will reject with the {@link ErrorInfo} that caused the suspension. However, + * the room will automatically retry attaching after a delay. + */ + suspend fun attach() + + /** + * Detaches from the room to stop receiving events in realtime. + */ + suspend fun detach() +} diff --git a/chat-android/src/main/java/com/ably/chat/RoomOptions.kt b/chat-android/src/main/java/com/ably/chat/RoomOptions.kt new file mode 100644 index 00000000..2263d937 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/RoomOptions.kt @@ -0,0 +1,74 @@ +package com.ably.chat + +/** + * Represents the options for a given chat room. + */ +data class RoomOptions( + /** + * The presence options for the room. To enable presence in the room, set this property. You may + * use {@link RoomOptionsDefaults.presence} to enable presence with default options. + * @defaultValue undefined + */ + val presence: PresenceOptions = PresenceOptions(), + + /** + * The typing options for the room. To enable typing in the room, set this property. You may use + * {@link RoomOptionsDefaults.typing} to enable typing with default options. + */ + val typing: TypingOptions = TypingOptions(), + + /** + * The reactions options for the room. To enable reactions in the room, set this property. You may use + * {@link RoomOptionsDefaults.reactions} to enable reactions with default options. + */ + val reactions: RoomReactionsOptions = RoomReactionsOptions(), + + /** + * The occupancy options for the room. To enable occupancy in the room, set this property. You may use + * {@link RoomOptionsDefaults.occupancy} to enable occupancy with default options. + */ + val occupancy: OccupancyOptions = OccupancyOptions(), +) + +/** + * Represents the presence options for a chat room. + */ +data class PresenceOptions( + /** + * Whether the underlying Realtime channel should use the presence enter mode, allowing entry into presence. + * This property does not affect the presence lifecycle, and users must still call {@link Presence.enter} + * in order to enter presence. + * @defaultValue true + */ + val enter: Boolean = true, + + /** + * Whether the underlying Realtime channel should use the presence subscribe mode, allowing subscription to presence. + * This property does not affect the presence lifecycle, and users must still call {@link Presence.subscribe} + * in order to subscribe to presence. + * @defaultValue true + */ + val subscribe: Boolean = true, +) + +/** + * Represents the typing options for a chat room. + */ +data class TypingOptions( + /** + * The timeout for typing events in milliseconds. If typing.start() is not called for this amount of time, a stop + * typing event will be fired, resulting in the user being removed from the currently typing set. + * @defaultValue 10000 + */ + val timeoutMs: Long = 10_000, +) + +/** + * Represents the reactions options for a chat room. + */ +class RoomReactionsOptions + +/** + * Represents the occupancy options for a chat room. + */ +class OccupancyOptions diff --git a/chat-android/src/main/java/com/ably/chat/RoomReactions.kt b/chat-android/src/main/java/com/ably/chat/RoomReactions.kt new file mode 100644 index 00000000..3ced2e2f --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/RoomReactions.kt @@ -0,0 +1,102 @@ +package com.ably.chat + +import io.ably.lib.realtime.Channel + +/** + * This interface is used to interact with room-level reactions in a chat room: subscribing to reactions and sending them. + * + * Get an instance via {@link Room.reactions}. + */ +interface RoomReactions : EmitsDiscontinuities { + /** + * Returns an instance of the Ably realtime channel used for room-level reactions. + * Avoid using this directly unless special features that cannot otherwise be implemented are needed. + * + * @returns The Ably realtime channel instance. + */ + val channel: Channel + + /** + * Send a reaction to the room including some metadata. + * + * This method accepts parameters for a room-level reaction. It accepts an object + * + * + * @param params an object containing {type, headers, metadata} for the room + * reaction to be sent. Type is required, metadata and headers are optional. + * @returns The returned promise resolves when the reaction was sent. Note + * that it is possible to receive your own reaction via the reactions + * listener before this promise resolves. + */ + suspend fun send(params: SendReactionParams) + + /** + * Subscribe to receive room-level reactions. + * + * @param listener The listener function to be called when a reaction is received. + * @returns A response object that allows you to control the subscription. + */ + fun subscribe(listener: Listener) + + /** + * Unsubscribe all listeners from receiving room-level reaction events. + */ + fun unsubscribe(listener: Listener) + + /** + * An interface for listening to new reaction events + */ + fun interface Listener { + /** + * A function that can be called when the new reaction happens. + * @param event The event that happened. + */ + fun onReaction(event: Reaction) + } +} + +/** + * Params for sending a room-level reactions. Only `type` is mandatory. + */ +data class SendReactionParams( + /** + * The type of the reaction, for example an emoji or a short string such as + * "like". + * + * It is the only mandatory parameter to send a room-level reaction. + */ + val type: String, + + /** + * Optional metadata of the reaction. + * + * The metadata is a map of extra information that can be attached to the + * room reaction. It is not used by Ably and is sent as part of the realtime + * message payload. Example use cases are custom animations or other effects. + * + * Do not use metadata for authoritative information. There is no server-side + * validation. When reading the metadata treat it like user input. + * + * The key `ably-chat` is reserved and cannot be used. Ably may populate this + * with different values in the future. + */ + val metadata: ReactionMetadata? = null, + + /** + * Optional headers of the room reaction. + * + * The headers are a flat key-value map and are sent as part of the realtime + * message's `extras` inside the `headers` property. They can serve similar + * purposes as the metadata but they are read by Ably and can be used for + * features such as + * [subscription filters](https://faqs.ably.com/subscription-filters). + * + * Do not use the headers for authoritative information. There is no + * server-side validation. When reading the headers treat them like user + * input. + * + * The key prefix `ably-chat` is reserved and cannot be used. Ably may add + * headers prefixed with `ably-chat` in the future. + */ + val headers: ReactionHeaders? = null, +) diff --git a/chat-android/src/main/java/com/ably/chat/RoomStatus.kt b/chat-android/src/main/java/com/ably/chat/RoomStatus.kt new file mode 100644 index 00000000..09f5b7a5 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/RoomStatus.kt @@ -0,0 +1,112 @@ +package com.ably.chat + +import io.ably.lib.types.ErrorInfo + +/** + * Represents the status of a Room. + */ +interface RoomStatus { + /** + * The current status of the room. + */ + val current: RoomLifecycle + + /** + * The current error, if any, that caused the room to enter the current status. + */ + val error: ErrorInfo? + + /** + * Registers a listener that will be called whenever the room status changes. + * @param listener The function to call when the status changes. + * @returns An object that can be used to unregister the listener. + */ + fun on(listener: Listener) + + /** + * Removes all listeners that were added by the `onChange` method. + */ + fun off(listener: Listener) + + /** + * An interface for listening to changes for the room status + */ + fun interface Listener { + /** + * A function that can be called when the room status changes. + * @param change The change in status. + */ + fun roomStatusChanged(change: RoomStatusChange) + } +} + +/** + * The different states that a room can be in throughout its lifecycle. + */ +enum class RoomLifecycle(val stateName: String) { + /** + * A temporary state for when the library is first initialized. + */ + Initialized("initialized"), + + /** + * The library is currently attempting to attach the room. + */ + Attaching("attaching"), + + /** + * The room is currently attached and receiving events. + */ + Attached("attached"), + + /** + * The room is currently detaching and will not receive events. + */ + Detaching("detaching"), + + /** + * The room is currently detached and will not receive events. + */ + Detached("detached"), + + /** + * The room is in an extended state of detachment, but will attempt to re-attach when able. + */ + Suspended("suspended"), + + /** + * The room is currently detached and will not attempt to re-attach. User intervention is required. + */ + Failed("failed"), + + /** + * The room is in the process of releasing. Attempting to use a room in this state may result in undefined behavior. + */ + Releasing("releasing"), + + /** + * The room has been released and is no longer usable. + */ + Released("released"), +} + +/** + * Represents a change in the status of the room. + */ +data class RoomStatusChange( + /** + * The new status of the room. + */ + val current: RoomLifecycle, + + /** + * The previous status of the room. + */ + val previous: RoomLifecycle, + + /** + * An error that provides a reason why the room has + * entered the new status, if applicable. + */ + val error: ErrorInfo? = null, +) diff --git a/chat-android/src/main/java/com/ably/chat/Typing.kt b/chat-android/src/main/java/com/ably/chat/Typing.kt new file mode 100644 index 00000000..c6532af5 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Typing.kt @@ -0,0 +1,79 @@ +package com.ably.chat + +import io.ably.lib.realtime.Channel + +/** + * base retry interval, we double it each time + */ +const val PRESENCE_GET_RETRY_INTERVAL_MS = 1500 + +/** + * max retry interval + */ +const val PRESENCE_GET_RETRY_MAX_INTERVAL_MS = 30_000 + +/** + * max num of retries + */ +const val PRESENCE_GET_MAX_RETRIES = 5 + +/** + * This interface is used to interact with typing in a chat room including subscribing to typing events and + * fetching the current set of typing clients. + * + * Get an instance via {@link Room.typing}. + */ +interface Typing : EmitsDiscontinuities { + /** + * Get the name of the realtime channel underpinning typing events. + * @returns The name of the realtime channel. + */ + val channel: Channel + + /** + * Subscribe a given listener to all typing events from users in the chat room. + * + * @param listener A listener to be called when the typing state of a user in the room changes. + */ + fun subscribe(listener: Listener) + + /** + * Unsubscribe listeners from receiving typing events. + */ + fun unsubscribe(listener: Listener) + + /** + * Get the current typers, a set of clientIds. + * @returns A Promise of a set of clientIds that are currently typing. + */ + suspend fun get(): Set + + /** + * Start indicates that the current user is typing. This will emit a typingStarted event to inform listening clients and begin a timer, + * once the timer expires, a typingStopped event will be emitted. The timeout is configurable through the typingTimeoutMs parameter. + * If the current user is already typing, it will reset the timer and being counting down again without emitting a new event. + */ + suspend fun start() + + /** + * Stop indicates that the current user has stopped typing. This will emit a typingStopped event to inform listening clients, + * and immediately clear the typing timeout timer. + */ + suspend fun stop() + + /** + * An interface for listening to changes for Typing + */ + fun interface Listener { + /** + * A function that can be called when the new typing event happens. + * @param event The event that happened. + */ + fun onEvent(event: TypingEvent) + } +} + +/** + * Represents a typing event. + */ +data class TypingEvent(val currentlyTyping: Set) diff --git a/detekt.yml b/detekt.yml index c1194c9b..cc8eda1b 100644 --- a/detekt.yml +++ b/detekt.yml @@ -571,7 +571,7 @@ naming: excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] FunctionMinLength: active: true - minimumFunctionNameLength: 3 + minimumFunctionNameLength: 2 FunctionNaming: active: true excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] @@ -1030,4 +1030,4 @@ style: WildcardImport: active: false # The same as NoWildcardImport from formatting excludeImports: - - 'java.util.*' \ No newline at end of file + - 'java.util.*' From 344e9f45147e73f7373bc50ff5b76f565c93004e Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Aug 2024 12:51:27 +0100 Subject: [PATCH 2/2] [ECO-4899] chore: rename `Direction` into more meaningful `MessageOrder` Also added TODO for `Messages#send` function, right now in the Chat JS documentation mentioned that it's behavior is non-deterministic, which is not very good for DX --- .../src/main/java/com/ably/chat/Messages.kt | 20 ++++++++++--------- .../src/main/java/com/ably/chat/Occupancy.kt | 3 +-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/chat-android/src/main/java/com/ably/chat/Messages.kt b/chat-android/src/main/java/com/ably/chat/Messages.kt index 37f12409..a43259c7 100644 --- a/chat-android/src/main/java/com/ably/chat/Messages.kt +++ b/chat-android/src/main/java/com/ably/chat/Messages.kt @@ -44,15 +44,17 @@ interface Messages : EmitsDiscontinuities { * * This method uses the Ably Chat API endpoint for sending messages. * - * Note that the Promise may resolve before OR after the message is received + * Note: that the suspending function may resolve before OR after the message is received * from the realtime channel. This means you may see the message that was just - * sent in a callback to `subscribe` before the returned promise resolves. + * sent in a callback to `subscribe` before the function resolves. + * + * TODO: Revisit this resolution policy during implementation (it will be much better for DX if this behavior is deterministic). * * @param params an object containing {text, headers, metadata} for the message * to be sent. Text is required, metadata and headers are optional. - * @returns A promise that resolves when the message was published. + * @returns The message was published. */ - fun send(params: SendMessageParams): Message + suspend fun send(params: SendMessageParams): Message /** * An interface for listening to new messaging event @@ -92,23 +94,23 @@ data class QueryOptions( val limit: Int = 100, /** - * The direction to query messages in. + * The order of messages in the query result. */ - val direction: Direction = Direction.FORWARDS, + val orderBy: MessageOrder = MessageOrder.NewestFirst, ) { /** * Represents direction to query messages in. */ - enum class Direction { + enum class MessageOrder { /** * The response will include messages from the start of the time window to the end. */ - FORWARDS, + NewestFirst, /** * the response will include messages from the end of the time window to the start. */ - BACKWARDS, + OldestFirst, } } diff --git a/chat-android/src/main/java/com/ably/chat/Occupancy.kt b/chat-android/src/main/java/com/ably/chat/Occupancy.kt index 60013529..0ebe721c 100644 --- a/chat-android/src/main/java/com/ably/chat/Occupancy.kt +++ b/chat-android/src/main/java/com/ably/chat/Occupancy.kt @@ -20,7 +20,6 @@ interface Occupancy : EmitsDiscontinuities { * Subscribe a given listener to occupancy updates of the chat room. * * @param listener A listener to be called when the occupancy of the room changes. - * @returns A promise resolves to the channel attachment state change event from the implicit channel attach operation. */ fun subscribe(listener: Listener) @@ -34,7 +33,7 @@ interface Occupancy : EmitsDiscontinuities { /** * Get the current occupancy of the chat room. * - * @returns A promise that resolves to the current occupancy of the chat room. + * @returns the current occupancy of the chat room. */ suspend fun get(): OccupancyEvent