Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
99556e7
Make ChatClient's options optional
lawrence-forooghian Oct 8, 2025
f0b5994
Fix copy-and-paste errors in test comment
lawrence-forooghian Oct 10, 2025
61c382e
Get rid of DiscontinuityEvent
lawrence-forooghian Oct 8, 2025
52372d5
Make get-typing-set API consistent with JS
lawrence-forooghian Oct 8, 2025
490ff28
Match JS's reactions headers and metadata type names
lawrence-forooghian Oct 8, 2025
f4592cb
Fix MessageReactionEventType name
lawrence-forooghian Oct 8, 2025
518eb4c
Get rid of RoomOptions.reactions
lawrence-forooghian Oct 8, 2025
ec489a9
Rename MessageAction to ChatMessageAction
lawrence-forooghian Oct 9, 2025
3b0fe9e
Prefix ChatMessageAction cases with `message`
lawrence-forooghian Oct 9, 2025
1f1de82
Make Message.reactions non-optional
lawrence-forooghian Oct 9, 2025
47f7c39
Split reaction event types in two
lawrence-forooghian Oct 9, 2025
5bc2faa
Make ConnectionStateChange.retryIn optional
lawrence-forooghian Oct 9, 2025
6ac1a16
Make MessageReactionRawEvent.timestamp non-optional
lawrence-forooghian Oct 9, 2025
6964536
Add all-events Presence subscription method
lawrence-forooghian Oct 9, 2025
8ce235d
Omit orderBy from historyBeforeSubscribe's params
lawrence-forooghian Oct 9, 2025
7236f4f
Restructure reaction summary events
lawrence-forooghian Oct 9, 2025
76119bf
Move reaction raw data type inside event
lawrence-forooghian Oct 9, 2025
c4c6d5a
Remove MessageReactionRawEvent.Reaction.isSelf
lawrence-forooghian Oct 9, 2025
7404bf1
Change operation metadata dictionary to have string values
lawrence-forooghian Oct 9, 2025
77adc61
Align Message.delete signature with JS at d778026
lawrence-forooghian Oct 10, 2025
c247613
Partially align Message.update signature with JS at d778026
lawrence-forooghian Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ struct ContentView: View {
List(listItems, id: \.id) { item in
switch item {
case let .message(messageItem):
if messageItem.message.action == .delete {
if messageItem.message.action == .messageDelete {
DeletedMessageView(item: messageItem)
.flip()
} else {
Expand Down Expand Up @@ -279,7 +279,7 @@ struct ContentView: View {

for message in previousMessages.items {
switch message.action {
case .create, .update, .delete:
case .messageCreate, .messageUpdate, .messageDelete:
withAnimation {
listItems.append(.message(.init(message: message, isSender: message.clientID == currentClientID)))
}
Expand All @@ -299,7 +299,7 @@ struct ContentView: View {
room.messages.reactions.subscribe { summaryEvent in
do {
try withAnimation {
if let reactedMessageItem = listItemWithMessageSerial(summaryEvent.summary.messageSerial) {
if let reactedMessageItem = listItemWithMessageSerial(summaryEvent.messageSerial) {
if let index = listItems.firstIndex(where: { $0.id == reactedMessageItem.message.serial }) {
listItems[index] = try .message(
.init(
Expand Down Expand Up @@ -403,15 +403,15 @@ struct ContentView: View {
return nil
}).first {
let editedMessage = editingMessageItem.message.copy(text: newMessage)
_ = try await room().messages.update(newMessage: editedMessage, description: nil, metadata: nil)
_ = try await room().messages.update(newMessage: editedMessage, details: nil)
}

newMessage = ""
}

func deleteMessage(_ message: Message) {
Task {
_ = try await room().messages.delete(message: message, params: .init())
_ = try await room().messages.delete(message: message, details: nil)
}
}

Expand Down
24 changes: 11 additions & 13 deletions Example/AblyChatExample/MessageViews/MessageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,21 @@ struct MessageView: View {
showReactionPicker = true
}
.padding(left: 2)
if item.message.action == .update {
if item.message.action == .messageUpdate {
Text("Edited")
.foregroundStyle(.gray)
.font(.footnote)
}
if let reactionsSummary = item.message.reactions {
MessageReactionSummaryView(
summary: reactionsSummary,
currentClientID: currentClientID,
onPickReaction: {
showReactionPicker = true
},
onAddReaction: onAddReaction,
onDeleteReaction: onDeleteReaction,
)
.padding(left: 2)
}
MessageReactionSummaryView(
summary: item.message.reactions,
currentClientID: currentClientID,
onPickReaction: {
showReactionPicker = true
},
onAddReaction: onAddReaction,
onDeleteReaction: onDeleteReaction,
)
.padding(left: 2)
}
Spacer()
if item.isSender {
Expand Down
3 changes: 2 additions & 1 deletion Example/AblyChatExample/Mocks/Misc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class MockMessagesPaginatedResult: PaginatedResult {
Array(repeating: 0, count: numberOfMockMessages).map { _ in
Message(
serial: "\(Date().timeIntervalSince1970)",
action: .create,
action: .messageCreate,
clientID: self.clientID,
text: MockStrings.randomPhrase(),
metadata: [:],
Expand All @@ -22,6 +22,7 @@ final class MockMessagesPaginatedResult: PaginatedResult {
timestamp: Date(),
),
timestamp: Date(),
reactions: .init(unique: [:], distinct: [:], multiple: [:]),
)
}
}
Expand Down
48 changes: 28 additions & 20 deletions Example/AblyChatExample/Mocks/MockClients.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class MockRoom: Room {
}

@discardableResult
func onDiscontinuity(_: @escaping @MainActor (DiscontinuityEvent) -> Void) -> DefaultStatusSubscription {
func onDiscontinuity(_: @escaping @MainActor (ARTErrorInfo) -> Void) -> DefaultStatusSubscription {
fatalError("Not yet implemented")
}
}
Expand All @@ -129,7 +129,7 @@ class MockMessages: Messages {
randomElement: {
let message = Message(
serial: "\(Date().timeIntervalSince1970)",
action: .create,
action: .messageCreate,
clientID: MockStrings.names.randomElement()!,
text: MockStrings.randomPhrase(),
metadata: [:],
Expand All @@ -139,6 +139,7 @@ class MockMessages: Messages {
timestamp: Date(),
),
timestamp: Date(),
reactions: .init(unique: [:], distinct: [:], multiple: [:]),
)
if byChance(30) { /* 30% of the messages will get the reaction */
self.reactions.messageSerials.append(message.serial)
Expand All @@ -161,7 +162,7 @@ class MockMessages: Messages {
func send(withParams params: SendMessageParams) async throws(ARTErrorInfo) -> Message {
let message = Message(
serial: "\(Date().timeIntervalSince1970)",
action: .create,
action: .messageCreate,
clientID: clientID,
text: params.text,
metadata: params.metadata ?? [:],
Expand All @@ -171,30 +172,32 @@ class MockMessages: Messages {
timestamp: Date(),
),
timestamp: Date(),
reactions: .init(unique: [:], distinct: [:], multiple: [:]),
)
mockSubscriptions.emit(ChatMessageEvent(message: message))
return message
}

func update(newMessage: Message, description _: String?, metadata _: OperationMetadata?) async throws(ARTErrorInfo) -> Message {
func update(newMessage: Message, details _: OperationDetails?) async throws(ARTErrorInfo) -> Message {
let message = Message(
serial: newMessage.serial,
action: .update,
action: .messageUpdate,
clientID: clientID,
text: newMessage.text,
metadata: newMessage.metadata,
headers: newMessage.headers,
version: .init(serial: "\(Date().timeIntervalSince1970)", timestamp: Date(), clientID: clientID),
timestamp: Date(),
reactions: .init(unique: [:], distinct: [:], multiple: [:]),
)
mockSubscriptions.emit(ChatMessageEvent(message: message))
return message
}

func delete(message: Message, params _: DeleteMessageParams) async throws(ARTErrorInfo) -> Message {
func delete(message: Message, details _: OperationDetails?) async throws(ARTErrorInfo) -> Message {
let message = Message(
serial: message.serial,
action: .delete,
action: .messageDelete,
clientID: clientID,
text: message.text,
metadata: message.metadata,
Expand All @@ -205,6 +208,7 @@ class MockMessages: Messages {
clientID: clientID,
),
timestamp: Date(),
reactions: .init(unique: [:], distinct: [:], multiple: [:]),
)
mockSubscriptions.emit(ChatMessageEvent(message: message))
return message
Expand All @@ -218,13 +222,12 @@ class MockMessageReactions: MessageReactions {
var clientIDs: Set<String> = []
var messageSerials: [String] = []

private var reactions: [MessageReaction] = []
private var reactions: [MessageReactionRawEvent.Reaction] = []

private let mockSubscriptions = MockSubscriptionStorage<MessageReactionSummaryEvent>()

private func getUniqueReactionsSummaryForMessage(_ messageSerial: String) -> MessageReactionSummary {
MessageReactionSummary(
messageSerial: messageSerial,
unique: [:],
distinct: reactions.filter { $0.messageSerial == messageSerial }.reduce(into: [String: MessageReactionSummary.ClientIDList]()) { dict, newItem in
if var oldItem = dict[newItem.name] {
Expand All @@ -248,19 +251,19 @@ class MockMessageReactions: MessageReactions {

func send(forMessageWithSerial messageSerial: String, params: SendMessageReactionParams) async throws(ARTErrorInfo) {
reactions.append(
MessageReaction(
MessageReactionRawEvent.Reaction(
type: .distinct,
name: params.name,
messageSerial: messageSerial,
count: params.count,
clientID: clientID,
isSelf: true,
),
)
mockSubscriptions.emit(
MessageReactionSummaryEvent(
type: MessageReactionEvent.summary,
summary: getUniqueReactionsSummaryForMessage(messageSerial),
type: MessageReactionSummaryEventType.summary,
messageSerial: messageSerial,
reactions: getUniqueReactionsSummaryForMessage(messageSerial),
),
)
}
Expand All @@ -271,8 +274,9 @@ class MockMessageReactions: MessageReactions {
}
mockSubscriptions.emit(
MessageReactionSummaryEvent(
type: MessageReactionEvent.summary,
summary: getUniqueReactionsSummaryForMessage(messageSerial),
type: MessageReactionSummaryEventType.summary,
messageSerial: messageSerial,
reactions: getUniqueReactionsSummaryForMessage(messageSerial),
),
)
}
Expand All @@ -284,18 +288,18 @@ class MockMessageReactions: MessageReactions {
return nil
}
self.reactions.append(
MessageReaction(
MessageReactionRawEvent.Reaction(
type: .distinct,
name: Emoji.random(),
messageSerial: messageSerial,
count: 1,
clientID: senderClientID,
isSelf: senderClientID == self.clientID,
),
)
return MessageReactionSummaryEvent(
type: MessageReactionEvent.summary,
summary: self.getUniqueReactionsSummaryForMessage(messageSerial),
type: MessageReactionSummaryEventType.summary,
messageSerial: messageSerial,
reactions: self.getUniqueReactionsSummaryForMessage(messageSerial),
)
},
interval: Double([Int](1 ... 10).randomElement()!) / 10.0,
Expand Down Expand Up @@ -381,7 +385,7 @@ class MockTyping: Typing {
)
}

func get() async throws(ARTErrorInfo) -> Set<String> {
var current: Set<String> {
Set(MockStrings.names.shuffled().prefix(2))
}

Expand Down Expand Up @@ -531,6 +535,10 @@ class MockPresence: Presence {
)
}

func subscribe(_ callback: @escaping @MainActor (PresenceEvent) -> Void) -> MockSubscription {
createSubscription(callback: callback)
}

func subscribe(event _: PresenceEventType, _ callback: @escaping @MainActor (PresenceEvent) -> Void) -> MockSubscription {
createSubscription(callback: callback)
}
Expand Down
10 changes: 5 additions & 5 deletions Example/AblyChatExample/Mocks/MockSubscriptionStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class MockMessageSubscriptionStorage<Element: Sendable, PaginatedResult: AblyCha

init(
randomElement: @escaping @MainActor @Sendable () -> Element,
previousMessages: @escaping @MainActor @Sendable (HistoryParams) async throws(ARTErrorInfo) -> PaginatedResult,
previousMessages: @escaping @MainActor @Sendable (HistoryBeforeSubscribeParams) async throws(ARTErrorInfo) -> PaginatedResult,
interval: @escaping @MainActor @Sendable () -> Double,
callback: @escaping @MainActor (Element) -> Void,
onTerminate: @escaping () -> Void,
Expand All @@ -152,7 +152,7 @@ class MockMessageSubscriptionStorage<Element: Sendable, PaginatedResult: AblyCha

func create(
randomElement: @escaping @MainActor @Sendable () -> Element,
previousMessages: @escaping @MainActor @Sendable (HistoryParams) async throws(ARTErrorInfo) -> PaginatedResult,
previousMessages: @escaping @MainActor @Sendable (HistoryBeforeSubscribeParams) async throws(ARTErrorInfo) -> PaginatedResult,
interval: @autoclosure @escaping @MainActor @Sendable () -> Double,
callback: @escaping @MainActor (Element) -> Void,
) -> some MessageSubscriptionResponse {
Expand Down Expand Up @@ -206,9 +206,9 @@ struct MockStatusSubscription: StatusSubscription {

struct MockMessageSubscriptionResponse<PaginatedResult: AblyChat.PaginatedResult<Message>>: MessageSubscriptionResponse {
private let _unsubscribe: () -> Void
private let previousMessages: @MainActor @Sendable (HistoryParams) async throws(ARTErrorInfo) -> PaginatedResult
private let previousMessages: @MainActor @Sendable (HistoryBeforeSubscribeParams) async throws(ARTErrorInfo) -> PaginatedResult

func historyBeforeSubscribe(withParams params: HistoryParams) async throws(ARTErrorInfo) -> PaginatedResult {
func historyBeforeSubscribe(withParams params: HistoryBeforeSubscribeParams) async throws(ARTErrorInfo) -> PaginatedResult {
try await previousMessages(params)
}

Expand All @@ -217,7 +217,7 @@ struct MockMessageSubscriptionResponse<PaginatedResult: AblyChat.PaginatedResult
}

init(
previousMessages: @escaping @MainActor @Sendable (HistoryParams) async throws(ARTErrorInfo) -> PaginatedResult,
previousMessages: @escaping @MainActor @Sendable (HistoryBeforeSubscribeParams) async throws(ARTErrorInfo) -> PaginatedResult,
unsubscribe: @MainActor @Sendable @escaping () -> Void,
) {
self.previousMessages = previousMessages
Expand Down
16 changes: 8 additions & 8 deletions Sources/AblyChat/ChatAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal final class ChatAPI {

// (CHA-M8) A client must be able to update a message in a room.
// (CHA-M8a) A client may update a message via the Chat REST API by calling the update method.
internal func updateMessage(roomName: String, with modifiedMessage: Message, description: String?, metadata: OperationMetadata?) async throws(InternalError) -> Message {
internal func updateMessage(roomName: String, with modifiedMessage: Message, details: OperationDetails?) async throws(InternalError) -> Message {
let endpoint = "\(apiVersionV4)/rooms/\(roomName)/messages/\(modifiedMessage.serial)"
var body: [String: JSONValue] = [:]
let messageObject: [String: JSONValue] = [
Expand All @@ -72,12 +72,12 @@ internal final class ChatAPI {

body["message"] = .object(messageObject)

if let description {
if let description = details?.description {
body["description"] = .string(description)
}

if let metadata {
body["metadata"] = .object(metadata)
if let metadata = details?.metadata {
body["metadata"] = .object(metadata.mapValues { .string($0) })
}

// (CHA-M8c) An update operation has PUT semantics. If a field is not specified in the update, it is assumed to be removed.
Expand All @@ -89,16 +89,16 @@ internal final class ChatAPI {

// (CHA-M9) A client must be able to delete a message in a room.
// (CHA-M9a) A client may delete a message via the Chat REST API by calling the delete method.
internal func deleteMessage(roomName: String, message: Message, params: DeleteMessageParams) async throws(InternalError) -> Message {
internal func deleteMessage(roomName: String, message: Message, details: OperationDetails?) async throws(InternalError) -> Message {
let endpoint = "\(apiVersionV4)/rooms/\(roomName)/messages/\(message.serial)/delete"
var body: [String: JSONValue] = [:]

if let description = params.description {
if let description = details?.description {
body["description"] = .string(description)
}

if let metadata = params.metadata {
body["metadata"] = .object(metadata)
if let metadata = details?.metadata {
body["metadata"] = .object(metadata.mapValues { .string($0) })
}

// (CHA-M9b) When a message is deleted successfully via the REST API, the caller shall receive a struct representing the Message in response, as if it were received via Realtime event.
Expand Down
2 changes: 1 addition & 1 deletion Sources/AblyChat/ChatClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public class ChatClient: ChatClientProtocol {
* - realtime: The Ably Realtime client. Its `dispatchQueue` option must be the main queue (this is its default behaviour).
* - clientOptions: The client options.
*/
public convenience init(realtime: ARTRealtime, clientOptions: ChatClientOptions?) {
public convenience init(realtime: ARTRealtime, clientOptions: ChatClientOptions? = nil) {
self.init(
realtime: realtime,
clientOptions: clientOptions,
Expand Down
4 changes: 2 additions & 2 deletions Sources/AblyChat/Connection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ public struct ConnectionStatusChange: Sendable {
/**
* The time in milliseconds that the client will wait before attempting to reconnect.
*/
public var retryIn: TimeInterval
public var retryIn: TimeInterval?

/// Memberwise initializer to create a `ConnectionStatusChange`.
///
/// - Note: You should not need to use this initializer when using the Chat SDK. It is exposed only to allow users to create mock versions of the SDK's protocols.
public init(current: ConnectionStatus, previous: ConnectionStatus, error: ARTErrorInfo? = nil, retryIn: TimeInterval) {
public init(current: ConnectionStatus, previous: ConnectionStatus, error: ARTErrorInfo? = nil, retryIn: TimeInterval?) {
self.current = current
self.previous = previous
self.error = error
Expand Down
1 change: 1 addition & 0 deletions Sources/AblyChat/DefaultConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal final class DefaultConnection: Connection {
current: currentState,
previous: previousState,
error: stateChange.reason,
// TODO: Actually emit `nil` when appropriate (we can't currently since ably-cocoa's corresponding property is mis-typed): https://github.com/ably/ably-chat-swift/issues/394
retryIn: stateChange.retryIn,
)

Expand Down
Loading