Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1416be6
Fix access level of an initializer
lawrence-forooghian Oct 2, 2025
6930dda
Remove unnecessary `@unchecked`
lawrence-forooghian Oct 2, 2025
ce15be3
Remove PaginatedResult's conformance to Equatable
lawrence-forooghian Oct 3, 2025
753cb6b
Mark PaginatedResult as @MainActor
lawrence-forooghian Oct 3, 2025
bc9de70
Add note allowing us to add ErrorCode values
lawrence-forooghian Oct 6, 2025
07cda47
Fix capitalisation of "clientID" in public API
lawrence-forooghian Oct 6, 2025
e85d229
Update comment
lawrence-forooghian Oct 6, 2025
10914f5
Change some struct properties from let to var for consistency
lawrence-forooghian Oct 6, 2025
928c4c3
Fix access level of an initializer
lawrence-forooghian Oct 6, 2025
81c267b
Add some missing public initializers
lawrence-forooghian Oct 6, 2025
bffe235
Remove dodgy MessageVersion Equatable implementation
lawrence-forooghian Oct 3, 2025
693b5ea
Remove Equatable from RoomStatusChange and DiscontinuityEvent
lawrence-forooghian Oct 3, 2025
4e3c55b
Add missing room status case helpers
lawrence-forooghian Oct 6, 2025
44b3bd1
Remove Identifiable conformance from Message
lawrence-forooghian Oct 3, 2025
5a99d1e
Remove Equatable from ChatMessageEvent
lawrence-forooghian Oct 3, 2025
cdf97f3
Remove Equatable conformance from all the options types
lawrence-forooghian Oct 3, 2025
e60ad5c
Remove public String-valued event type enums
lawrence-forooghian Oct 3, 2025
464bf10
Make message reactions send / delete signatures clearer
lawrence-forooghian Oct 6, 2025
f8a37a5
Remove `reactions` from Message.copy
lawrence-forooghian Oct 6, 2025
b2d84bf
Clarify documentation of Message.copy
lawrence-forooghian Oct 6, 2025
09fa18c
Use fatalError for fetching occupancy data when not switched on
lawrence-forooghian Oct 6, 2025
a334284
Fix copy-and-paste error
lawrence-forooghian Oct 6, 2025
52fe739
Remove parameter label from Message.with
lawrence-forooghian Oct 2, 2025
9fc668d
Fix "undefined" in copy of JS documentation
lawrence-forooghian Oct 7, 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
18 changes: 9 additions & 9 deletions Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ struct ContentView: View {
var id: String {
switch self {
case let .message(item):
item.message.id
item.message.serial
case let .presence(item):
item.presence.member.updatedAt.description
}
Expand Down Expand Up @@ -119,9 +119,9 @@ struct ContentView: View {
currentClientID: currentClientID,
item: messageItem,
isEditing: Binding(get: {
editingItemID == messageItem.message.id
editingItemID == messageItem.message.serial
}, set: { editing in
editingItemID = editing ? messageItem.message.id : nil
editingItemID = editing ? messageItem.message.serial : nil
newMessage = editing ? messageItem.message.text : ""
}),
onDeleteMessage: {
Expand Down Expand Up @@ -265,7 +265,7 @@ struct ContentView: View {
)
}
case .updated, .deleted:
if let index = listItems.firstIndex(where: { $0.id == message.id }) {
if let index = listItems.firstIndex(where: { $0.id == message.serial }) {
listItems[index] = .message(
.init(
message: message,
Expand Down Expand Up @@ -300,10 +300,10 @@ struct ContentView: View {
do {
try withAnimation {
if let reactedMessageItem = listItemWithMessageSerial(summaryEvent.summary.messageSerial) {
if let index = listItems.firstIndex(where: { $0.id == reactedMessageItem.message.id }) {
if let index = listItems.firstIndex(where: { $0.id == reactedMessageItem.message.serial }) {
listItems[index] = try .message(
.init(
message: reactedMessageItem.message.with(summaryEvent: summaryEvent),
message: reactedMessageItem.message.with(summaryEvent),
isSender: reactedMessageItem.message.clientID == currentClientID,
),
)
Expand Down Expand Up @@ -397,7 +397,7 @@ struct ContentView: View {
}

if let editingMessageItem = listItems.compactMap({ listItem -> MessageListItem? in
if case let .message(message) = listItem, message.message.id == editingItemID {
if case let .message(message) = listItem, message.message.serial == editingItemID {
return message
}
return nil
Expand All @@ -423,13 +423,13 @@ struct ContentView: View {

func addMessageReaction(_ reaction: String, messageSerial: String) {
Task {
try await room().messages.reactions.send(to: messageSerial, params: .init(name: reaction, type: .distinct))
try await room().messages.reactions.send(messageSerial: messageSerial, params: .init(name: reaction, type: .distinct))
}
}

func deleteMessageReaction(_ reaction: String, messageSerial: String) {
Task {
try await room().messages.reactions.delete(from: messageSerial, params: .init(name: reaction, type: .distinct))
try await room().messages.reactions.delete(messageSerial: messageSerial, params: .init(name: reaction, type: .distinct))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct MessageReactionSummaryView: View {

private let maxReactionsCount = 5

var reactions: [String: MessageReactionSummary.ClientIdList] {
var reactions: [String: MessageReactionSummary.ClientIDList] {
summary.distinct
}

Expand Down Expand Up @@ -64,7 +64,7 @@ struct MessageReactionSummaryView: View {
showAllReactionsSheet = true
}
if let emoji = selectedEmoji {
if reactions[emoji]?.clientIds.contains(currentClientID) ?? false {
if reactions[emoji]?.clientIDs.contains(currentClientID) ?? false {
Button("Remove my \(emoji)", role: .destructive) {
onDeleteReaction(emoji)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ struct MessageReactionsSheet: View {
@Environment(\.dismiss)
private var dismiss

init(uniqueOrDistinct: [String: MessageReactionSummary.ClientIdList]) {
init(uniqueOrDistinct: [String: MessageReactionSummary.ClientIDList]) {
for (emoji, clientList) in uniqueOrDistinct {
for clientId in clientList.clientIds {
for clientId in clientList.clientIDs {
let key = "\(clientId)-\(emoji)"
reactions[key] = ReactionItem(
emoji: emoji,
Expand Down
4 changes: 0 additions & 4 deletions Example/AblyChatExample/Mocks/Misc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ final class MockMessagesPaginatedResult: PaginatedResult {
self.roomName = roomName
self.numberOfMockMessages = numberOfMockMessages
}

static func == (_: MockMessagesPaginatedResult, _: MockMessagesPaginatedResult) -> Bool {
fatalError("Not implemented")
}
}

enum MockStrings {
Expand Down
20 changes: 10 additions & 10 deletions Example/AblyChatExample/Mocks/MockClients.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,15 +226,15 @@ class MockMessageReactions: MessageReactions {
MessageReactionSummary(
messageSerial: messageSerial,
unique: [:],
distinct: reactions.filter { $0.messageSerial == messageSerial }.reduce(into: [String: MessageReactionSummary.ClientIdList]()) { dict, newItem in
distinct: reactions.filter { $0.messageSerial == messageSerial }.reduce(into: [String: MessageReactionSummary.ClientIDList]()) { dict, newItem in
if var oldItem = dict[newItem.name] {
if !oldItem.clientIds.contains(newItem.clientID) {
oldItem.clientIds.append(newItem.clientID)
if !oldItem.clientIDs.contains(newItem.clientID) {
oldItem.clientIDs.append(newItem.clientID)
oldItem.total += 1
}
dict[newItem.name] = oldItem
} else {
dict[newItem.name] = MessageReactionSummary.ClientIdList(total: 1, clientIds: [newItem.clientID], clipped: false)
dict[newItem.name] = MessageReactionSummary.ClientIDList(total: 1, clientIDs: [newItem.clientID], clipped: false)
}
},
multiple: [:],
Expand All @@ -246,7 +246,7 @@ class MockMessageReactions: MessageReactions {
self.roomName = roomName
}

func send(to messageSerial: String, params: SendMessageReactionParams) async throws(ARTErrorInfo) {
func send(messageSerial: String, params: SendMessageReactionParams) async throws(ARTErrorInfo) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's not "sending message serial", it's sending a reaction to the message with message serial. Ditto for delete.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that the existing signature makes it clear what the argument means, though. It's just a String without any additional information.

I agree that you're not "sending a message serial", the same way that in:

  • Messages.send(params: SendMessageParams) you're sending a message, not "sending a params"
  • Presence.enter(data: PresenceData) you're not "entering a data"

etc.

And I'm not 100% sure what's the right signature for these things in Swift. Like, would the following be better, or just more verbose? I don't know.

  • MessageReactions.send(forMessageWithSerial:)
  • Messages.send(withParams:)
  • Presence.enter(withData:)

etc

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key part of the API design guidelines though is "Prefer method and function names that make use sites form grammatical English phrases."

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So maybe the latter are better?

Copy link
Collaborator

@maratal maratal Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, more descriptive and reflects things correctly. Feel free to resolve this once you decide the names.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK — can I do that one in a separate PR just so I don't have to deal with merge conflicts on #378?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gonna merge this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also consider this: MessageReactions.send(for messageWithSerial:) @lawrence-forooghian but I'm not sure, leaving you to decide.

reactions.append(
MessageReaction(
type: .distinct,
Expand All @@ -265,7 +265,7 @@ class MockMessageReactions: MessageReactions {
)
}

func delete(from messageSerial: String, params: DeleteMessageReactionParams) async throws(ARTErrorInfo) {
func delete(messageSerial: String, params: DeleteMessageReactionParams) async throws(ARTErrorInfo) {
reactions.removeAll { reaction in
reaction.messageSerial == messageSerial && reaction.name == params.name && reaction.clientID == clientID
}
Expand Down Expand Up @@ -373,7 +373,7 @@ class MockTyping: Typing {
MockStrings.names.randomElement()!,
MockStrings.names.randomElement()!,
],
change: .init(clientId: MockStrings.names.randomElement()!, type: .started),
change: .init(clientID: MockStrings.names.randomElement()!, type: .started),
)
},
interval: 2,
Expand All @@ -390,7 +390,7 @@ class MockTyping: Typing {
TypingSetEvent(
type: .setChanged,
currentlyTyping: [clientID],
change: .init(clientId: clientID, type: .started),
change: .init(clientID: clientID, type: .started),
),
)
}
Expand All @@ -400,7 +400,7 @@ class MockTyping: Typing {
TypingSetEvent(
type: .setChanged,
currentlyTyping: [],
change: .init(clientId: clientID, type: .stopped),
change: .init(clientID: clientID, type: .stopped),
),
)
}
Expand Down Expand Up @@ -568,7 +568,7 @@ class MockOccupancy: Occupancy {
OccupancyData(connections: 10, presenceMembers: 5)
}

func current() throws(ARTErrorInfo) -> AblyChat.OccupancyData? {
var current: AblyChat.OccupancyData? {
OccupancyData(connections: 10, presenceMembers: 5)
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/AblyChat/ChatAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal final class ChatAPI {
private let realtime: any InternalRealtimeClientProtocol
private let apiVersionV4 = "/chat/v4"

public init(realtime: any InternalRealtimeClientProtocol) {
internal init(realtime: any InternalRealtimeClientProtocol) {
self.realtime = realtime
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/AblyChat/DefaultMessageReactions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal final class DefaultMessageReactions: MessageReactions {
}

// (CHA-MR4) Users should be able to send a reaction to a message via the `send` method of the `MessagesReactions` object
internal func send(to messageSerial: String, params: SendMessageReactionParams) async throws(ARTErrorInfo) {
internal func send(messageSerial: String, params: SendMessageReactionParams) async throws(ARTErrorInfo) {
do {
var count = params.count
if params.type == .multiple, params.count == nil {
Expand All @@ -40,7 +40,7 @@ internal final class DefaultMessageReactions: MessageReactions {
}

// (CHA-MR11) Users should be able to delete a reaction from a message via the `delete` method of the `MessagesReactions` object
internal func delete(from messageSerial: String, params: DeleteMessageReactionParams) async throws(ARTErrorInfo) {
internal func delete(messageSerial: String, params: DeleteMessageReactionParams) async throws(ARTErrorInfo) {
let reactionType = params.type ?? options.defaultMessageReactionType
if reactionType != .unique, params.name == nil {
throw ARTErrorInfo(chatError: .unableDeleteReactionWithoutName(reactionType: reactionType.rawValue))
Expand Down
11 changes: 6 additions & 5 deletions Sources/AblyChat/DefaultOccupancy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal final class DefaultOccupancy: Occupancy {
internal func subscribe(_ callback: @escaping @MainActor (OccupancyEvent) -> Void) -> some Subscription {
// CHA-O4e (we use a fatalError for this programmer error, which is the idiomatic thing to do for Swift)
guard options.enableEvents else {
fatalError("In order to be able to subscribe to presence events, please set enableEvents to true in the room's occupancy options.")
fatalError("In order to be able to subscribe to occupancy events, please set enableEvents to true in the room's occupancy options.")
}

logger.log(message: "Subscribing to occupancy events", level: .debug)
Expand Down Expand Up @@ -68,11 +68,12 @@ internal final class DefaultOccupancy: Occupancy {
}

// (CHA-O7a) The current method should return the latest occupancy numbers received over the realtime connection in a [meta]occupancy event.
internal func current() throws(ARTErrorInfo) -> OccupancyData? {
// CHA-O7c
if !options.enableEvents {
throw ARTErrorInfo(chatError: .occupancyEventsNotEnabled)
internal var current: OccupancyData? {
// CHA-O7c (we use a fatalError for this programmer error, which is the idiomatic thing to do for Swift)
guard options.enableEvents else {
fatalError("In order to be able to fetch the last received occupancy event, please set enableEvents to true in the room's occupancy options.")
}

// CHA-07a
// CHA-07b
return lastOccupancyData
Expand Down
6 changes: 3 additions & 3 deletions Sources/AblyChat/DefaultTyping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ internal final class DefaultTyping: Typing {
TypingSetEvent(
type: .setChanged,
currentlyTyping: typingTimerManager.currentlyTypingClientIDs(),
change: .init(clientId: messageClientID, type: .stopped),
change: .init(clientID: messageClientID, type: .stopped),
),
)
}
Expand All @@ -67,7 +67,7 @@ internal final class DefaultTyping: Typing {
TypingSetEvent(
type: .setChanged,
currentlyTyping: typingTimerManager.currentlyTypingClientIDs(),
change: .init(clientId: messageClientID, type: .started),
change: .init(clientID: messageClientID, type: .started),
),
)
}
Expand All @@ -90,7 +90,7 @@ internal final class DefaultTyping: Typing {
TypingSetEvent(
type: .setChanged,
currentlyTyping: typingTimerManager.currentlyTypingClientIDs(),
change: .init(clientId: messageClientID, type: .stopped),
change: .init(clientID: messageClientID, type: .stopped),
),
)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/AblyChat/DiscontinuityEvent.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Ably

public struct DiscontinuityEvent: Sendable, Equatable {
public struct DiscontinuityEvent: Sendable {
/// The error associated with this discontinuity.
public var error: ARTErrorInfo

Expand Down
10 changes: 3 additions & 7 deletions Sources/AblyChat/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public let errorDomain = "AblyChatErrorDomain"

/**
The error codes for errors in the ``errorDomain`` error domain.

- Note: Future minor version updates of the library may add new values to this enum. Bear this in mind if you wish to switch exhaustively over it.
*/
public enum ErrorCode: Int {
/// The user attempted to perform an invalid action.
Expand Down Expand Up @@ -177,7 +179,6 @@ internal enum ChatError {
case clientIdRequired
case attachSerialIsNotDefined
case channelFailedToAttach(cause: ARTErrorInfo?)
case occupancyEventsNotEnabled

internal var codeAndStatusCode: ErrorCodeAndStatusCode {
switch self {
Expand Down Expand Up @@ -216,8 +217,6 @@ internal enum ChatError {
.fixedStatusCode(.badRequest)
case .channelFailedToAttach:
.fixedStatusCode(.badRequest)
case .occupancyEventsNotEnabled:
.fixedStatusCode(.badRequest)
}
}

Expand Down Expand Up @@ -278,8 +277,6 @@ internal enum ChatError {
"Channel is attached, but attachSerial is not defined."
case let .channelFailedToAttach(cause):
"Channel failed to attach: \(String(describing: cause))"
case .occupancyEventsNotEnabled:
"Cannot perform operation because occupancy events are not enabled for this room."
}
}

Expand All @@ -304,8 +301,7 @@ internal enum ChatError {
.messageRejectedByBeforePublishRule,
.messageRejectedByModeration,
.clientIdRequired,
.attachSerialIsNotDefined,
.occupancyEventsNotEnabled:
.attachSerialIsNotDefined:
nil
}
}
Expand Down
6 changes: 0 additions & 6 deletions Sources/AblyChat/InternalError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ internal enum InternalError: Error {
}
}

extension InternalError: Equatable {
internal static func == (lhs: InternalError, rhs: InternalError) -> Bool {
lhs.toARTErrorInfo() == rhs.toARTErrorInfo()
}
}

internal extension ARTErrorInfo {
func toInternalError() -> InternalError {
.errorInfo(self)
Expand Down
Loading