Skip to content

Commit cee81de

Browse files
committed
refactor: Change read receipts API to user-centric [String: Long] and add fetchReadReceipts
1 parent 4f49cbf commit cee81de

File tree

10 files changed

+165
-25
lines changed

10 files changed

+165
-25
lines changed

PubNubSwiftChatSDK/PubNubSwiftChatSDK.docc/Channel.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
- ``streamUpdatesOn(channels:callback:)``
1111
- ``streamReadReceipts()``
1212
- ``streamReadReceipts(callback:)``
13+
- ``fetchReadReceipts(limit:page:filter:sort:)``
14+
- ``fetchReadReceipts(limit:page:filter:sort:completion:)``
1315
- ``streamMessageReports()``
1416
- ``streamMessageReports(callback:)``
1517
- ``streamPresence()``

PubNubSwiftChatSDK/PubNubSwiftChatSDK.docc/ChannelImpl.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
- ``streamUpdatesOn(channels:callback:)``
1111
- ``streamReadReceipts()``
1212
- ``streamReadReceipts(callback:)``
13+
- ``fetchReadReceipts(limit:page:filter:sort:)``
14+
- ``fetchReadReceipts(limit:page:filter:sort:completion:)``
1315
- ``streamMessageReports()``
1416
- ``streamMessageReports(callback:)``
1517
- ``streamPresence()``

Snippets/MessageSnippets.swift

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,8 +1270,8 @@ func streamReadReceiptsAsyncStream() {
12701270
Task {
12711271
if let channel = try await chat.getChannel(channelId: "support") {
12721272
for await readReceipt in channel.streamReadReceipts() {
1273-
readReceipt.forEach { (messageTimetoken, users) in
1274-
debugPrint("Message timetoken: \(messageTimetoken) was read by users: \(users)")
1273+
readReceipt.forEach { (userId, timetoken) in
1274+
debugPrint("User \(userId) has read up to timetoken: \(timetoken)")
12751275
}
12761276
}
12771277
} else {
@@ -1288,8 +1288,29 @@ func streamReadReceiptsClosure() {
12881288
// Assumes a "ChannelImpl" reference named "channel"
12891289
autoCloseable = channel.streamReadReceipts { receipts in
12901290
print("Read Receipts Received:")
1291-
receipts.forEach { (messageTimetoken, users) in
1292-
print("Message Timetoken: \(messageTimetoken) was read by users: \(users)")
1291+
receipts.forEach { (userId, timetoken) in
1292+
print("User \(userId) has read up to timetoken: \(timetoken)")
1293+
}
1294+
}
1295+
// snippet.end
1296+
}
1297+
1298+
// MARK: - Fetch Read Receipts
1299+
1300+
func fetchReadReceipts() {
1301+
// snippet.messages.readReceipts.fetch
1302+
// Assumes a "ChatImpl" reference named "chat"
1303+
Task {
1304+
if let channel = try await chat.getChannel(channelId: "support") {
1305+
let result = try await channel.fetchReadReceipts()
1306+
// Dictionary mapping user IDs to the timetoken they have read up to
1307+
let receipts = result.receipts
1308+
// Use for subsequent call: channel.fetchReadReceipts(page: nextPage)
1309+
let nextPage = result.page
1310+
1311+
receipts.forEach { (userId, timetoken) in
1312+
print("User \(userId) has read up to timetoken: \(timetoken)")
1313+
}
12931314
}
12941315
}
12951316
// snippet.end

Sources/Entities/BaseChannel.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -479,21 +479,41 @@ final class BaseChannel<C: PubNubChat.Channel_, M: PubNubChat.Message>: Channel
479479
)
480480
}
481481

482-
func streamReadReceipts(callback: @escaping (([Timetoken: [String]]) -> Void)) -> AutoCloseable {
482+
func streamReadReceipts(callback: @escaping (([String: Timetoken]) -> Void)) -> AutoCloseable {
483483
AutoCloseableImpl(
484484
channel.streamReadReceipts { [weak self] in
485485
if self != nil {
486-
callback(
487-
$0.reduce(into: [Timetoken: [String]]()) { res, currentItem in
488-
res[Timetoken(currentItem.key.uint64Value)] = currentItem.value
489-
}
490-
)
486+
callback($0.mapValues {
487+
Timetoken($0.uint64Value)
488+
})
491489
}
492490
},
493491
owner: self
494492
)
495493
}
496494

495+
func fetchReadReceipts(
496+
limit: Int?,
497+
page: PubNubHashedPage?,
498+
filter: String?,
499+
sort: [PubNub.MembershipSortField],
500+
completion: ((Swift.Result<(receipts: [String: Timetoken], page: PubNubHashedPage?), Error>) -> Void)?
501+
) {
502+
getMembers(limit: limit, page: page, filter: filter, sort: sort) { result in
503+
switch result {
504+
case let .success((memberships, page)):
505+
let receipts = memberships.reduce(into: [String: Timetoken]()) { result, membership in
506+
if let timetoken = membership.lastReadMessageTimetoken {
507+
result[membership.user.id] = timetoken
508+
}
509+
}
510+
completion?(.success((receipts: receipts, page: page)))
511+
case let .failure(error):
512+
completion?(.failure(error))
513+
}
514+
}
515+
}
516+
497517
func getFiles(
498518
limit: Int,
499519
next: String?,

Sources/Entities/Channel+AsyncAwait.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ public extension Channel {
303303
/// - limit: Number of objects to return in response
304304
/// - page: Object used for pagination to define which previous or next result page you want to fetch
305305
/// - filter: Expression used to filter the results. Returns only these members whose properties satisfy the given expression
306-
/// - sort: A collection to specify the sort order. Available options are id, name, and updated
306+
/// - sort: A collection to specify the sort order
307307
/// - Returns: A `Tuple` containing an array of the members of the channel, and the next pagination `PubNubHashedPage` (if one exists)
308308
func getMembers(
309309
limit: Int? = nil,
@@ -525,7 +525,7 @@ public extension Channel {
525525
}
526526

527527
/// Lets you get a read confirmation status for messages you published on a channel.
528-
func streamReadReceipts() -> AsyncStream<[Timetoken: [String]]> {
528+
func streamReadReceipts() -> AsyncStream<[String: Timetoken]> {
529529
AsyncStream { continuation in
530530
let autoCloseable = streamReadReceipts {
531531
continuation.yield($0)
@@ -536,6 +536,32 @@ public extension Channel {
536536
}
537537
}
538538

539+
/// Fetches the read receipts for members of this channel.
540+
///
541+
/// - Parameters:
542+
/// - limit: Number of objects to return in response
543+
/// - page: Object used for pagination to define which previous or next result page you want to fetch
544+
/// - filter: Expression used to filter the results. Returns only these members whose properties satisfy the given expression
545+
/// - sort: A collection to specify the sort order
546+
/// - Returns: A `Tuple` containing a dictionary mapping user IDs to the timetoken they have read up to, and the next pagination `PubNubHashedPage` (if one exists)
547+
func fetchReadReceipts(
548+
limit: Int? = nil,
549+
page: PubNubHashedPage? = nil,
550+
filter: String? = nil,
551+
sort: [PubNub.MembershipSortField] = []
552+
) async throws -> (receipts: [String: Timetoken], page: PubNubHashedPage?) {
553+
try await withCheckedThrowingContinuation { continuation in
554+
fetchReadReceipts(limit: limit, page: page, filter: filter, sort: sort) {
555+
switch $0 {
556+
case let .success(result):
557+
continuation.resume(returning: result)
558+
case let .failure(error):
559+
continuation.resume(throwing: error)
560+
}
561+
}
562+
}
563+
}
564+
539565
/// Returns all files attached to messages on a given channel.
540566
///
541567
/// - Parameters:

Sources/Entities/Channel.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public protocol Channel: CustomStringConvertible {
273273
/// - limit: Number of objects to return in response
274274
/// - page: Object used for pagination to define which previous or next result page you want to fetch
275275
/// - filter: Expression used to filter the results. Returns only these members whose properties satisfy the given expression
276-
/// - sort: A collection to specify the sort order. Available options are id, name, and updated
276+
/// - sort: A collection to specify the sort order
277277
/// - completion: The async `Result` of the method call
278278
/// - **Success**: A `Tuple` containing an array of the members of the channel, and the next pagination `PubNubHashedPage` (if one exists)
279279
/// - **Failure**: An `Error` describing the failure
@@ -428,12 +428,30 @@ public protocol Channel: CustomStringConvertible {
428428
/// - Important: Keep a strong reference to the returned ``AutoCloseable`` object as long as you want to receive updates. If ``AutoCloseable`` is deallocated,
429429
/// the stream will be canceled, and no further items will be produced. You can also stop receiving updates manually by calling ``AutoCloseable/close()``.
430430
///
431-
/// - Parameter callback: Defines the custom behavior to be executed when receiving a read confirmation status on the joined channel.
431+
/// - Parameter callback: Defines the custom behavior to be executed when receiving a read confirmation status on the joined channel. The callback receives a dictionary mapping user IDs to the timetoken they have read up to.
432432
/// - Returns: AutoCloseable Interface you can call to stop listening for message read receipts and clean up resources by invoking the close() method
433433
func streamReadReceipts(
434-
callback: @escaping (([Timetoken: [String]]) -> Void)
434+
callback: @escaping (([String: Timetoken]) -> Void)
435435
) -> AutoCloseable
436436

437+
/// Fetches the read receipts for members of this channel.
438+
///
439+
/// - Parameters:
440+
/// - limit: Number of objects to return in response
441+
/// - page: Object used for pagination to define which previous or next result page you want to fetch
442+
/// - filter: Expression used to filter the results. Returns only these members whose properties satisfy the given expression
443+
/// - sort: A collection to specify the sort order
444+
/// - completion: The async `Result` of the method call
445+
/// - **Success**: A `Tuple` containing a dictionary mapping user IDs to the timetoken they have read up to, and the next pagination `PubNubHashedPage` (if one exists)
446+
/// - **Failure**: An `Error` describing the failure
447+
func fetchReadReceipts(
448+
limit: Int?,
449+
page: PubNubHashedPage?,
450+
filter: String?,
451+
sort: [PubNub.MembershipSortField],
452+
completion: ((Swift.Result<(receipts: [String: Timetoken], page: PubNubHashedPage?), Error>) -> Void)?
453+
)
454+
437455
/// Returns all files attached to messages on a given channel.
438456
///
439457
/// - Parameters:

Sources/Entities/ChannelImpl.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,12 +354,28 @@ extension ChannelImpl: Channel {
354354
)
355355
}
356356

357-
public func streamReadReceipts(callback: @escaping (([Timetoken: [String]]) -> Void)) -> AutoCloseable {
357+
public func streamReadReceipts(callback: @escaping (([String: Timetoken]) -> Void)) -> AutoCloseable {
358358
target.streamReadReceipts(
359359
callback: callback
360360
)
361361
}
362362

363+
public func fetchReadReceipts(
364+
limit: Int? = nil,
365+
page: PubNubHashedPage? = nil,
366+
filter: String? = nil,
367+
sort: [PubNub.MembershipSortField] = [],
368+
completion: ((Swift.Result<(receipts: [String: Timetoken], page: PubNubHashedPage?), Error>) -> Void)? = nil
369+
) {
370+
target.fetchReadReceipts(
371+
limit: limit,
372+
page: page,
373+
filter: filter,
374+
sort: sort,
375+
completion: completion
376+
)
377+
}
378+
363379
public func getFiles(
364380
limit: Int = 100,
365381
next: String? = nil,

Sources/Entities/ThreadChannelImpl.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,12 +391,28 @@ extension ThreadChannelImpl: ThreadChannel {
391391
)
392392
}
393393

394-
public func streamReadReceipts(callback: @escaping (([Timetoken: [String]]) -> Void)) -> AutoCloseable {
394+
public func streamReadReceipts(callback: @escaping (([String: Timetoken]) -> Void)) -> AutoCloseable {
395395
target.streamReadReceipts(
396396
callback: callback
397397
)
398398
}
399399

400+
public func fetchReadReceipts(
401+
limit: Int? = nil,
402+
page: PubNubHashedPage? = nil,
403+
filter: String? = nil,
404+
sort: [PubNub.MembershipSortField] = [],
405+
completion: ((Swift.Result<(receipts: [String: Timetoken], page: PubNubHashedPage?), Error>) -> Void)? = nil
406+
) {
407+
target.fetchReadReceipts(
408+
limit: limit,
409+
page: page,
410+
filter: filter,
411+
sort: sort,
412+
completion: completion
413+
)
414+
}
415+
400416
public func getFiles(
401417
limit: Int = 100,
402418
next: String? = nil,

Tests/AsyncAwait/ChannelAsyncIntegrationTests.swift

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -507,10 +507,8 @@ class ChannelAsyncIntegrationTests: BaseAsyncIntegrationTestCase {
507507

508508
let task = Task {
509509
for await readReceipt in channel.streamReadReceipts() {
510-
XCTAssertEqual(readReceipt[timetoken]?.count, 1)
511-
XCTAssertEqual(readReceipt[timetoken]?.first, currentUserId)
512-
XCTAssertEqual(readReceipt[secondTimetoken]?.count, 1)
513-
XCTAssertEqual(readReceipt[secondTimetoken]?.first, anotherUserId)
510+
XCTAssertEqual(readReceipt[currentUserId], timetoken)
511+
XCTAssertEqual(readReceipt[anotherUserId], secondTimetoken)
514512
expectation.fulfill()
515513
}
516514
}
@@ -523,6 +521,29 @@ class ChannelAsyncIntegrationTests: BaseAsyncIntegrationTestCase {
523521
}
524522
}
525523

524+
func testChannelAsync_FetchReadReceipts() async throws {
525+
let anotherUser = try await chat.createUser(
526+
user: UserImpl(chat: chat, id: randomString())
527+
)
528+
let membership = try await channel.invite(user: chat.currentUser)
529+
try await Task.sleep(nanoseconds: 1_000_000_000)
530+
let anotherMembership = try await channel.invite(user: anotherUser)
531+
532+
let timetoken = try XCTUnwrap(membership.lastReadMessageTimetoken)
533+
let secondTimetoken = try XCTUnwrap(anotherMembership.lastReadMessageTimetoken)
534+
let currentUserId = chat.currentUser.id
535+
let anotherUserId = anotherUser.id
536+
537+
let (receipts, _) = try await channel.fetchReadReceipts()
538+
539+
XCTAssertEqual(receipts[currentUserId], timetoken)
540+
XCTAssertEqual(receipts[anotherUserId], secondTimetoken)
541+
542+
addTeardownBlock { [unowned self] in
543+
_ = try? await chat.deleteUser(id: anotherUserId)
544+
}
545+
}
546+
526547
func testChannelAsync_GetFiles() async throws {
527548
let fileUrlSession = URLSession(
528549
configuration: URLSessionConfiguration.default,

Tests/ChannelIntegrationTests.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -768,10 +768,8 @@ class ChannelIntegrationTests: BaseClosureIntegrationTestCase {
768768
let anotherUserId = anotherUser.id
769769

770770
let closeable = channel.streamReadReceipts {
771-
XCTAssertEqual($0[timetoken]?.count, 1)
772-
XCTAssertEqual($0[timetoken]?.first, currentUserId)
773-
XCTAssertEqual($0[secondTimetoken]?.count, 1)
774-
XCTAssertEqual($0[secondTimetoken]?.first, anotherUserId)
771+
XCTAssertEqual($0[currentUserId], timetoken)
772+
XCTAssertEqual($0[anotherUserId], secondTimetoken)
775773
expectation.fulfill()
776774
}
777775

0 commit comments

Comments
 (0)