Skip to content

Commit 9c1ec6d

Browse files
AndyTWFclaude
andcommitted
Add TypingMember type, currentTypers property, and bump ably-cocoa to 1.2.58
- Bump ably-cocoa dependency from 1.2.51 to 1.2.58 for userClaim support - Add TypingMember struct pairing clientID with userClaim metadata - Add currentTypers property to Typing protocol and TypingSetEvent - Add currentlyTypingMembers() to TypingTimerManager - Consolidate createSandboxChatClient/createSandboxChatClientWithJWT into a single helper with optional JWT parameters - Add JWT auth helper (Sandbox.createJWT) for integration tests - Add integration test for userClaim propagation across all event types - Stub new ARTChannelProtocol methods from ably-cocoa 1.2.58 in mock Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 21a404a commit 9c1ec6d

14 files changed

+347
-23
lines changed

Package.resolved

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ let package = Package(
2626
// This is the SDK's only dependency.
2727
.package(
2828
url: "https://github.com/ably/ably-cocoa",
29-
from: "1.2.51",
29+
from: "1.2.58",
3030
),
3131

3232
// All of the following dependencies are only used for internal purposes (testing or build tooling).

Sources/AblyChat/DefaultTyping.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ internal final class DefaultTyping: Typing {
6666
TypingSetEvent(
6767
type: .setChanged,
6868
currentlyTyping: typingTimerManager.currentlyTypingClientIDs(),
69+
currentTypers: typingTimerManager.currentlyTypingMembers(),
6970
change: .init(clientID: messageClientID, type: .stopped, userClaim: expiredUserClaim),
7071
),
7172
)
@@ -78,6 +79,7 @@ internal final class DefaultTyping: Typing {
7879
TypingSetEvent(
7980
type: .setChanged,
8081
currentlyTyping: typingTimerManager.currentlyTypingClientIDs(),
82+
currentTypers: typingTimerManager.currentlyTypingMembers(),
8183
change: .init(clientID: messageClientID, type: .started, userClaim: typingTimerManager.userClaimForClient(messageClientID)),
8284
),
8385
)
@@ -109,6 +111,7 @@ internal final class DefaultTyping: Typing {
109111
TypingSetEvent(
110112
type: .setChanged,
111113
currentlyTyping: typingTimerManager.currentlyTypingClientIDs(),
114+
currentTypers: typingTimerManager.currentlyTypingMembers(),
112115
change: .init(clientID: messageClientID, type: .stopped, userClaim: userClaim),
113116
),
114117
)
@@ -131,6 +134,11 @@ internal final class DefaultTyping: Typing {
131134
typingTimerManager.currentlyTypingClientIDs()
132135
}
133136

137+
// (CHA-T18)
138+
internal var currentTypers: [TypingMember] {
139+
typingTimerManager.currentlyTypingMembers()
140+
}
141+
134142
// (CHA-T4) Users may indicate that they have started typing using the keystroke method.
135143
internal func keystroke() async throws(ErrorInfo) {
136144
try await keyboardOperationQueue.enqueue { [weak self] () throws(ErrorInfo) in

Sources/AblyChat/Message.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ public struct Message: Sendable, Equatable {
8484
*/
8585
public var reactions: MessageReactionSummary
8686

87+
// @spec CHA-M2h
8788
/**
8889
* The user claim attached to this message by the server.
8990
*
9091
* Set automatically by the Ably server when a JWT contains a matching
9192
* `ably.room.<roomName>` claim. This is a read-only, server-provided value.
9293
*/
93-
// @spec CHA-M2h
9494
public var userClaim: String?
9595

9696
/// Memberwise initializer to create a `Message`.

Sources/AblyChat/MessageReaction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,13 @@ public struct MessageReactionRawEvent: Sendable {
280280
*/
281281
public var clientID: String
282282

283+
// @spec CHA-MR7d
283284
/**
284285
* The user claim attached to this reaction event by the server.
285286
*
286287
* Set automatically by the Ably server when a JWT contains a matching
287288
* `ably.room.<roomName>` claim. This is a read-only, server-provided value.
288289
*/
289-
// @spec CHA-MR7d
290290
public var userClaim: String?
291291

292292
/// Memberwise initializer to create a `Reaction`.

Sources/AblyChat/Presence.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,13 @@ public struct PresenceMember: Sendable {
182182
// swiftlint:disable:next missing_docs
183183
public var updatedAt: Date
184184

185+
// @spec CHA-PR6g
185186
/**
186187
* The user claim attached to this presence event by the server.
187188
*
188189
* Set automatically by the Ably server when a JWT contains a matching
189190
* `ably.room.<roomName>` claim. This is a read-only, server-provided value.
190191
*/
191-
// @spec CHA-PR6g
192192
public var userClaim: String?
193193
}
194194

Sources/AblyChat/RoomReaction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ public struct RoomReaction: Sendable {
4646
*/
4747
public var isSelf: Bool
4848

49+
// @spec CHA-ER2a
4950
/**
5051
* The user claim attached to this reaction by the server.
5152
*
5253
* Set automatically by the Ably server when a JWT contains a matching
5354
* `ably.room.<roomName>` claim. This is a read-only, server-provided value.
5455
*/
55-
// @spec CHA-ER2a
5656
public var userClaim: String?
5757

5858
/// Memberwise initializer to create a `RoomReaction`.

Sources/AblyChat/Typing.swift

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
import Ably
22

3+
/**
4+
* Represents a user in the set of currently typing users, with associated metadata.
5+
*/
6+
public struct TypingMember: Sendable {
7+
/// The client ID of the typing user.
8+
public var clientID: String
9+
10+
/// The user claim attached to this user's typing event, if any.
11+
public var userClaim: String?
12+
13+
/// Memberwise initializer to create a `TypingMember`.
14+
///
15+
/// - 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.
16+
public init(clientID: String, userClaim: String? = nil) {
17+
self.clientID = clientID
18+
self.userClaim = userClaim
19+
}
20+
}
21+
322
/**
423
* This interface is used to interact with typing in a chat room including subscribing to typing events and
524
* fetching the current set of typing clients.
@@ -22,13 +41,24 @@ public protocol Typing: AnyObject, Sendable {
2241
@discardableResult
2342
func subscribe(_ callback: @escaping @MainActor (TypingSetEvent) -> Void) -> Subscription
2443

44+
// @spec CHA-T16
2545
/**
2646
* Get the current typers, a set of clientIds.
2747
*
48+
* Deprecated per CHA-T16; use ``currentTypers`` (CHA-T18) instead.
49+
*
2850
* - Returns: A set of clientIds that are currently typing.
2951
*/
3052
var current: Set<String> { get }
3153

54+
// @spec CHA-T18
55+
/**
56+
* Gets the current set of users who are typing, with associated metadata.
57+
*
58+
* - Returns: An array of ``TypingMember`` for users currently typing.
59+
*/
60+
var currentTypers: [TypingMember] { get }
61+
3262
/**
3363
* Keystroke indicates that the current user is typing. This will emit a ``TypingEvent`` event to inform listening clients and begin a timer,
3464
* once the timer expires, another ``TypingEvent`` event will be emitted. In both cases ``TypingEvent/currentlyTyping``
@@ -95,6 +125,12 @@ public struct TypingSetEvent: Sendable {
95125
*/
96126
public var currentlyTyping: Set<String>
97127

128+
// @spec CHA-T6d
129+
/**
130+
* The set of users currently typing, with associated metadata.
131+
*/
132+
public var currentTypers: [TypingMember]
133+
98134
/**
99135
* Get the details of the operation that modified the typing event.
100136
*/
@@ -103,9 +139,10 @@ public struct TypingSetEvent: Sendable {
103139
/// Memberwise initializer to create a `TypingSetEvent`.
104140
///
105141
/// - 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.
106-
public init(type: TypingSetEventType, currentlyTyping: Set<String>, change: Change) {
142+
public init(type: TypingSetEventType, currentlyTyping: Set<String>, currentTypers: [TypingMember], change: Change) {
107143
self.type = type
108144
self.currentlyTyping = currentlyTyping
145+
self.currentTypers = currentTypers
109146
self.change = change
110147
}
111148

@@ -116,6 +153,7 @@ public struct TypingSetEvent: Sendable {
116153
// swiftlint:disable:next missing_docs
117154
public var type: TypingEventType
118155

156+
// @spec CHA-T13a1
119157
/**
120158
* The user claim attached to this typing event by the server.
121159
*
@@ -125,7 +163,6 @@ public struct TypingSetEvent: Sendable {
125163
* The `userClaim` must persist across heartbeat events and inactivity timeouts
126164
* for a given `clientId`.
127165
*/
128-
// @spec CHA-T13a1
129166
public var userClaim: String?
130167

131168
/// Memberwise initializer to create a `Change`.

Sources/AblyChat/TypingTimerManager.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ internal final class TypingTimerManager<AnyClock: ClockProtocol>: TypingTimerMan
9999
Set(whoIsTypingState.keys)
100100
}
101101

102+
/// Returns the currently typing users with associated metadata.
103+
internal func currentlyTypingMembers() -> [TypingMember] {
104+
whoIsTypingState.map { clientID, state in
105+
TypingMember(clientID: clientID, userClaim: state.userClaim)
106+
}
107+
}
108+
102109
/// Returns the stored `userClaim` for a given client, if any (CHA-T13a1).
103110
internal func userClaimForClient(_ clientID: String) -> String? {
104111
whoIsTypingState[clientID]?.userClaim
@@ -126,6 +133,8 @@ internal protocol TypingTimerManagerProtocol {
126133
func isCurrentlyTyping(clientID: String) -> Bool
127134
/// Returns the set of client IDs that we consider to currently be typing (also referred to in the spec as the "typing set").
128135
func currentlyTypingClientIDs() -> Set<String>
136+
/// Returns the currently typing users with associated metadata.
137+
func currentlyTypingMembers() -> [TypingMember]
129138
/// Returns the stored `userClaim` for a given client, if any (CHA-T13a1).
130139
func userClaimForClient(_ clientID: String) -> String?
131140
}

Tests/AblyChatTests/DefaultTypingTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ struct DefaultTypingTests {
8181

8282
// @specOneOf(1/2) CHA-T6a - Tests subscription receives started event
8383
// @specOneOf(1/2) CHA-T4a3 - Tests that publish has correct name and data
84+
// @specOneOf(1/2) CHA-T6d - Tests that currentTypers is populated in started event
8485
@Test
8586
@available(iOS 16.0, tvOS 16.0, *)
8687
func subscribe_ReceivesStartedTypingEvent() async throws {
@@ -94,6 +95,7 @@ struct DefaultTypingTests {
9495
TypingSetEvent(
9596
type: .setChanged,
9697
currentlyTyping: [clientId],
98+
currentTypers: [.init(clientID: clientId)],
9799
change: .init(clientID: clientId, type: .started),
98100
),
99101
)
@@ -103,9 +105,12 @@ struct DefaultTypingTests {
103105
#expect(typingEvent.change.type == .started)
104106
#expect(typingEvent.change.clientID == clientId)
105107
#expect(typingEvent.currentlyTyping == [clientId])
108+
#expect(typingEvent.currentTypers.count == 1)
109+
#expect(typingEvent.currentTypers[0].clientID == clientId)
106110
}
107111

108112
// @specOneOf(2/2) CHA-T6a - Tests subscription receives stopped event
113+
// @specOneOf(2/2) CHA-T6d - Tests that currentTypers is populated in stopped event
109114
@Test
110115
@available(iOS 16.0, tvOS 16.0, *)
111116
func subscribe_ReceivesStoppedTypingEvent() async throws {
@@ -119,6 +124,7 @@ struct DefaultTypingTests {
119124
TypingSetEvent(
120125
type: .setChanged,
121126
currentlyTyping: [],
127+
currentTypers: [],
122128
change: .init(clientID: clientId, type: .stopped),
123129
),
124130
)
@@ -128,9 +134,12 @@ struct DefaultTypingTests {
128134
#expect(typingEvent.change.type == .stopped)
129135
#expect(typingEvent.change.clientID == clientId)
130136
#expect(typingEvent.currentlyTyping.isEmpty)
137+
#expect(typingEvent.currentTypers.isEmpty)
131138
}
132139

133140
// @spec CHA-T9 - Tests retrieving currently typing clients
141+
// @spec CHA-T16 - Tests current property returns typing client IDs
142+
// @spec CHA-T18 - Tests currentTypers property returns typing members
134143
@Test
135144
@available(iOS 16.0, tvOS 16.0, *)
136145
func get_ReturnsCurrentlyTypingClients() async throws {
@@ -147,9 +156,12 @@ struct DefaultTypingTests {
147156

148157
// When
149158
let typingClients = typing.current
159+
let typingMembers = typing.currentTypers
150160

151161
// Then
152162
#expect(typingClients.contains("test-client"))
163+
#expect(typingMembers.count == 1)
164+
#expect(typingMembers[0].clientID == "test-client")
153165
}
154166

155167
// @specOneOf(2/2) CHA-T4a3 - Tests that publish has ephemeral flag

0 commit comments

Comments
 (0)