diff --git a/Package.swift b/Package.swift index 4701430..c27a80c 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/GetStream/stream-core-swift.git", exact: "0.1.0") + .package(url: "https://github.com/GetStream/stream-core-swift.git", exact: "0.2.1") ], targets: [ .target( diff --git a/Sources/StreamFeeds/Models/ActivityData.swift b/Sources/StreamFeeds/Models/ActivityData.swift index bd16beb..3494f2d 100644 --- a/Sources/StreamFeeds/Models/ActivityData.swift +++ b/Sources/StreamFeeds/Models/ActivityData.swift @@ -18,6 +18,7 @@ public struct ActivityData: Identifiable, Equatable, Sendable { public let expiresAt: Date? public let feeds: [String] public let filterTags: [String] + public let hidden: Bool public let id: String public let interestTags: [String] public private(set) var latestReactions: [FeedsReactionData] @@ -106,6 +107,12 @@ extension ActivityData { } } + mutating func deleteComment(_ comment: CommentData) { + if comments.remove(byId: comment.id) { + commentCount = max(0, commentCount - 1) + } + } + mutating func updateComment(_ incomingData: CommentData) { comments.updateFirstElement(where: { $0.id == incomingData.id }, changes: { $0.merge(with: incomingData) }) } @@ -130,50 +137,6 @@ extension ActivityData { changes: { $0.merge(with: incomingData, update: reaction, currentUserId: currentUserId) } ) } - - // MARK: - - - mutating func deleteComment(_ comment: CommentData) { - if comments.remove(byId: comment.id) { - commentCount = max(0, commentCount - 1) - } - } - - mutating func addBookmark(_ bookmark: BookmarkData, currentUserId: String) { - if bookmark.user.id == currentUserId { - if ownBookmarks.insert(byId: bookmark) { - bookmarkCount += 1 - } - } else { - bookmarkCount += 1 - } - } - - mutating func deleteBookmark(_ bookmark: BookmarkData, currentUserId: String) { - if bookmark.user.id == currentUserId { - if ownBookmarks.remove(byId: bookmark.id) { - bookmarkCount = max(0, bookmarkCount - 1) - } - } else { - bookmarkCount = max(0, bookmarkCount - 1) - } - } - - mutating func addReaction(_ reaction: FeedsReactionData, currentUserId: String) { - FeedsReactionData.updateByAdding(reaction: reaction, to: &latestReactions, reactionGroups: &reactionGroups) - reactionCount = reactionGroups.values.reduce(0) { $0 + $1.count } - if reaction.user.id == currentUserId { - ownReactions.insert(byId: reaction) - } - } - - mutating func removeReaction(_ reaction: FeedsReactionData, currentUserId: String) { - FeedsReactionData.updateByRemoving(reaction: reaction, from: &latestReactions, reactionGroups: &reactionGroups) - reactionCount = reactionGroups.values.reduce(0) { $0 + $1.count } - if reaction.user.id == currentUserId { - ownReactions.remove(byId: reaction.id) - } - } } // MARK: - Model Conversions @@ -193,6 +156,7 @@ extension ActivityResponse { expiresAt: expiresAt, feeds: feeds, filterTags: filterTags, + hidden: hidden ?? false, id: id, interestTags: interestTags, latestReactions: latestReactions.map { $0.toModel() }, diff --git a/Sources/StreamFeeds/Models/ActivityPinData.swift b/Sources/StreamFeeds/Models/ActivityPinData.swift index c2d6871..f145c29 100644 --- a/Sources/StreamFeeds/Models/ActivityPinData.swift +++ b/Sources/StreamFeeds/Models/ActivityPinData.swift @@ -7,6 +7,7 @@ import Foundation public struct ActivityPinData: Equatable, Sendable { public internal(set) var activity: ActivityData public let createdAt: Date + public let duration: String? public let feed: FeedId public let updatedAt: Date public let userId: String @@ -25,6 +26,7 @@ extension ActivityPinResponse { ActivityPinData( activity: activity.toModel(), createdAt: createdAt, + duration: nil, // ActivityPinResponse doesn't have duration feed: FeedId(rawValue: feed), updatedAt: updatedAt, userId: user.id @@ -37,6 +39,7 @@ extension PinActivityResponse { ActivityPinData( activity: activity.toModel(), createdAt: createdAt, + duration: duration, feed: FeedId(rawValue: feed), updatedAt: createdAt, // no updatedAt userId: userId diff --git a/Sources/StreamFeeds/Models/AggregatedActivityData.swift b/Sources/StreamFeeds/Models/AggregatedActivityData.swift index 3d6772e..c867f63 100644 --- a/Sources/StreamFeeds/Models/AggregatedActivityData.swift +++ b/Sources/StreamFeeds/Models/AggregatedActivityData.swift @@ -13,6 +13,7 @@ public struct AggregatedActivityData: Identifiable, Equatable, Sendable { public var score: Float public var updatedAt: Date public var userCount: Int + public var userCountTruncated: Bool public var id: String { if let first = activities.first?.id { @@ -32,7 +33,8 @@ extension AggregatedActivityResponse { group: group, score: score, updatedAt: updatedAt, - userCount: userCount + userCount: userCount, + userCountTruncated: userCountTruncated ) } } diff --git a/Sources/StreamFeeds/Models/BookmarkData.swift b/Sources/StreamFeeds/Models/BookmarkData.swift index 6d26e0d..e193e50 100644 --- a/Sources/StreamFeeds/Models/BookmarkData.swift +++ b/Sources/StreamFeeds/Models/BookmarkData.swift @@ -16,7 +16,7 @@ public struct BookmarkData: Equatable, Sendable { extension BookmarkData: Identifiable { public var id: String { - activity.id + user.id + "\(user.id)-\(activity.id)" } } diff --git a/Sources/StreamFeeds/Models/CommentData.swift b/Sources/StreamFeeds/Models/CommentData.swift index a2e5045..ba667c2 100644 --- a/Sources/StreamFeeds/Models/CommentData.swift +++ b/Sources/StreamFeeds/Models/CommentData.swift @@ -172,22 +172,6 @@ extension CommentData { ownReactions.replace(byId: reaction) } - mutating func addReaction(_ reaction: FeedsReactionData, currentUserId: String) { - FeedsReactionData.updateByAdding(reaction: reaction, to: &latestReactions, reactionGroups: &reactionGroups) - reactionCount = reactionGroups.values.reduce(0) { $0 + $1.count } - if reaction.user.id == currentUserId { - ownReactions.append(reaction) - } - } - - mutating func removeReaction(_ reaction: FeedsReactionData, currentUserId: String) { - FeedsReactionData.updateByRemoving(reaction: reaction, from: &latestReactions, reactionGroups: &reactionGroups) - reactionCount = reactionGroups.values.reduce(0) { $0 + $1.count } - if reaction.user.id == currentUserId { - ownReactions.remove(byId: reaction.id) - } - } - mutating func updateUser(_ incomingData: UserData) { mentionedUsers.updateAll(where: { $0.id == incomingData.id }, changes: { $0 = incomingData }) user = incomingData diff --git a/Sources/StreamFeeds/Models/FeedData.swift b/Sources/StreamFeeds/Models/FeedData.swift index b3e432b..eefda6d 100644 --- a/Sources/StreamFeeds/Models/FeedData.swift +++ b/Sources/StreamFeeds/Models/FeedData.swift @@ -19,10 +19,12 @@ public struct FeedData: Identifiable, Equatable, Sendable { public let id: String public let memberCount: Int public let name: String + public let ownCapabilities: [FeedOwnCapability]? + public let ownFollows: [FollowData]? + public let ownMembership: FeedMemberData? public let pinCount: Int public let updatedAt: Date public let visibility: String? - public let ownCapabilities: [FeedOwnCapability]? var localFilterData: LocalFilterData? } @@ -45,10 +47,12 @@ extension FeedResponse { id: id, memberCount: memberCount, name: name, + ownCapabilities: ownCapabilities, + ownFollows: ownFollows?.map { $0.toModel() }, + ownMembership: ownMembership?.toModel(), pinCount: pinCount, updatedAt: updatedAt, - visibility: visibility, - ownCapabilities: ownCapabilities + visibility: visibility ) } } diff --git a/Sources/StreamFeeds/Models/FeedMemberData.swift b/Sources/StreamFeeds/Models/FeedMemberData.swift index ac6dcce..7502df9 100644 --- a/Sources/StreamFeeds/Models/FeedMemberData.swift +++ b/Sources/StreamFeeds/Models/FeedMemberData.swift @@ -10,6 +10,7 @@ public struct FeedMemberData: Equatable, Sendable { public let custom: [String: RawJSON]? public let inviteAcceptedAt: Date? public let inviteRejectedAt: Date? + public let membershipLevel: MembershipLevelResponse? public let role: String public let status: FeedMemberStatus public let updatedAt: Date @@ -35,6 +36,7 @@ extension FeedMemberResponse { custom: custom, inviteAcceptedAt: inviteAcceptedAt, inviteRejectedAt: inviteRejectedAt, + membershipLevel: membershipLevel, role: role, status: status, updatedAt: updatedAt, diff --git a/Sources/StreamFeeds/Models/FeedsReactionData.swift b/Sources/StreamFeeds/Models/FeedsReactionData.swift index e02289f..31368e5 100644 --- a/Sources/StreamFeeds/Models/FeedsReactionData.swift +++ b/Sources/StreamFeeds/Models/FeedsReactionData.swift @@ -7,6 +7,7 @@ import StreamCore public struct FeedsReactionData: Equatable, Sendable { public let activityId: String + public let commentId: String? public let createdAt: Date public let custom: [String: RawJSON]? public let type: String @@ -16,40 +17,17 @@ public struct FeedsReactionData: Equatable, Sendable { extension FeedsReactionData: Identifiable { public var id: String { - "\(user.id)-\(type)-\(activityId)" + if let commentId { + "\(user.id)-\(type)-\(commentId)-\(activityId)" + } else { + "\(user.id)-\(type)-\(activityId)" + } } } // MARK: - Mutating the Data extension FeedsReactionData { - static func updateByAdding( - reaction: FeedsReactionData, - to latestReactions: inout [FeedsReactionData], - reactionGroups: inout [String: ReactionGroupData] - ) { - guard latestReactions.insert(byId: reaction) else { return } - var reactionGroup = reactionGroups[reaction.type] ?? ReactionGroupData(count: 1, firstReactionAt: reaction.createdAt, lastReactionAt: reaction.createdAt) - reactionGroup.increment(with: reaction.createdAt) - reactionGroups[reaction.type] = reactionGroup - } - - static func updateByRemoving( - reaction: FeedsReactionData, - from latestReactions: inout [FeedsReactionData], - reactionGroups: inout [String: ReactionGroupData] - ) { - guard latestReactions.remove(byId: reaction.id) else { return } - if var reactionGroup = reactionGroups[reaction.type] { - reactionGroup.decrement(with: reaction.createdAt) - if !reactionGroup.isEmpty { - reactionGroups[reaction.type] = reactionGroup - } else { - reactionGroups.removeValue(forKey: reaction.type) - } - } - } - mutating func updateUser(_ incomingData: UserData) { user = incomingData } @@ -61,6 +39,7 @@ extension FeedsReactionResponse { func toModel() -> FeedsReactionData { FeedsReactionData( activityId: activityId, + commentId: commentId, createdAt: createdAt, custom: custom, type: type, diff --git a/Sources/StreamFeeds/Models/PollData.swift b/Sources/StreamFeeds/Models/PollData.swift index b94da93..8f7a708 100644 --- a/Sources/StreamFeeds/Models/PollData.swift +++ b/Sources/StreamFeeds/Models/PollData.swift @@ -74,47 +74,6 @@ extension PollData { mutating func updateOption(_ option: PollOptionData) { options.replace(byId: option) } - - // MARK: - Votes - - mutating func castVote(_ vote: PollVoteData, currentUserId: String) { - if enforceUniqueVote, vote.user?.id == currentUserId { - for ownVote in ownVotes { - removeVote(ownVote, currentUserId: currentUserId) - } - ownVotes = [vote] - } else { - ownVotes.insert(byId: vote) - } - - var optionVotes = latestVotesByOption[vote.optionId] ?? [] - let optionVoteCount = optionVotes.count - optionVotes.insert(byId: vote) - latestVotesByOption[vote.optionId] = optionVotes - - if optionVotes.count != optionVoteCount { - let optionVoteCounts = voteCountsByOption[vote.optionId] ?? 0 - voteCountsByOption[vote.optionId] = optionVoteCounts + 1 - } - - voteCount = voteCountsByOption.reduce(0) { $0 + $1.value } - } - - mutating func removeVote(_ vote: PollVoteData, currentUserId: String) { - if vote.user?.id == currentUserId { - ownVotes.remove(byId: vote.id) - } - - var optionVotes = latestVotesByOption[vote.optionId] ?? [] - let optionVoteCount = optionVotes.count - optionVotes.remove(byId: vote.id) - latestVotesByOption[vote.optionId] = optionVotes - - if optionVotes.count != optionVoteCount { - let optionVoteCounts = voteCountsByOption[vote.optionId] ?? 0 - voteCountsByOption[vote.optionId] = max(0, optionVoteCounts - 1) - } - } } // MARK: - Model Conversions diff --git a/Sources/StreamFeeds/Models/ReactionGroupData.swift b/Sources/StreamFeeds/Models/ReactionGroupData.swift index 4b3801e..efcc955 100644 --- a/Sources/StreamFeeds/Models/ReactionGroupData.swift +++ b/Sources/StreamFeeds/Models/ReactionGroupData.swift @@ -9,21 +9,7 @@ public struct ReactionGroupData: Equatable, Sendable { public private(set) var count: Int public let firstReactionAt: Date public private(set) var lastReactionAt: Date -} - -extension ReactionGroupData { - var isEmpty: Bool { count <= 0 } - - mutating func decrement(with date: Date) { - guard date >= firstReactionAt || date <= lastReactionAt else { return } - count = max(0, count - 1) - } - - mutating func increment(with date: Date) { - guard date > firstReactionAt else { return } - count += 1 - lastReactionAt = date - } + public let sumScores: Int? } // MARK: - Model Conversions @@ -33,7 +19,8 @@ extension ReactionGroupResponse { ReactionGroupData( count: count, firstReactionAt: firstReactionAt, - lastReactionAt: lastReactionAt + lastReactionAt: lastReactionAt, + sumScores: sumScores ) } } diff --git a/Sources/StreamFeeds/Models/ThreadedCommentData.swift b/Sources/StreamFeeds/Models/ThreadedCommentData.swift index b21485e..cdc8a60 100644 --- a/Sources/StreamFeeds/Models/ThreadedCommentData.swift +++ b/Sources/StreamFeeds/Models/ThreadedCommentData.swift @@ -249,22 +249,6 @@ extension ThreadedCommentData { user = incomingData } - mutating func addReaction(_ reaction: FeedsReactionData, currentUserId: String) { - FeedsReactionData.updateByAdding(reaction: reaction, to: &latestReactions, reactionGroups: &reactionGroups) - reactionCount = reactionGroups.values.reduce(0) { $0 + $1.count } - if reaction.user.id == currentUserId { - ownReactions.append(reaction) - } - } - - mutating func removeReaction(_ reaction: FeedsReactionData, currentUserId: String) { - FeedsReactionData.updateByRemoving(reaction: reaction, from: &latestReactions, reactionGroups: &reactionGroups) - reactionCount = reactionGroups.values.reduce(0) { $0 + $1.count } - if reaction.user.id == currentUserId { - ownReactions.remove(byId: reaction.id) - } - } - // MARK: - Replies mutating func addReply(_ comment: ThreadedCommentData, sort areInIncreasingOrder: (ThreadedCommentData, ThreadedCommentData) -> Bool) { @@ -292,39 +276,6 @@ extension ThreadedCommentData { replies.sortedReplace(comment, nesting: nil, sorting: areInIncreasingOrder) self.replies = replies } - - // MARK: - Updating - - mutating func setCommentData(_ comment: CommentData) { - self = ThreadedCommentData( - attachments: comment.attachments, - confidenceScore: comment.confidenceScore, - controversyScore: comment.controversyScore, - createdAt: comment.createdAt, - custom: comment.custom, - deletedAt: comment.deletedAt, - downvoteCount: comment.downvoteCount, - id: comment.id, - latestReactions: comment.latestReactions, - mentionedUsers: comment.mentionedUsers, - meta: meta, - moderation: comment.moderation, - objectId: comment.objectId, - objectType: comment.objectType, - ownReactions: comment.ownReactions, - parentId: comment.parentId, - reactionCount: comment.reactionCount, - reactionGroups: comment.reactionGroups, - replies: replies, - replyCount: comment.replyCount, - score: comment.score, - status: comment.status, - text: comment.text, - updatedAt: comment.updatedAt, - upvoteCount: comment.upvoteCount, - user: comment.user - ) - } } // MARK: - Sorting diff --git a/Sources/StreamFeeds/Models/UserData.swift b/Sources/StreamFeeds/Models/UserData.swift index 643b106..9c33221 100644 --- a/Sources/StreamFeeds/Models/UserData.swift +++ b/Sources/StreamFeeds/Models/UserData.swift @@ -6,6 +6,7 @@ import Foundation import StreamCore public struct UserData: Identifiable, Equatable, Sendable, Hashable { + public let avgResponseTime: Int? public let banned: Bool public let blockedUserIds: [String] public let createdAt: Date @@ -35,6 +36,7 @@ public struct UserData: Identifiable, Equatable, Sendable, Hashable { extension UserResponse { func toModel() -> UserData { UserData( + avgResponseTime: avgResponseTime, banned: banned, blockedUserIds: blockedUserIds, createdAt: createdAt, @@ -59,6 +61,7 @@ extension UserResponse { extension UserResponseCommonFields { func toModel() -> UserData { UserData( + avgResponseTime: nil, // UserResponseCommonFields doesn't have avgResponseTime banned: banned, blockedUserIds: blockedUserIds, createdAt: createdAt, @@ -83,6 +86,7 @@ extension UserResponseCommonFields { extension UserResponsePrivacyFields { func toModel() -> UserData { UserData( + avgResponseTime: nil, // UserResponsePrivacyFields doesn't have avgResponseTime banned: banned, blockedUserIds: blockedUserIds, createdAt: createdAt, diff --git a/Sources/StreamFeeds/StateLayer/Common/StateLayerEventPublisher.swift b/Sources/StreamFeeds/StateLayer/Common/StateLayerEventPublisher.swift index 5ed7ed1..607fea9 100644 --- a/Sources/StreamFeeds/StateLayer/Common/StateLayerEventPublisher.swift +++ b/Sources/StreamFeeds/StateLayer/Common/StateLayerEventPublisher.swift @@ -47,6 +47,16 @@ final class StateLayerEventPublisher: WSEventsSubscriber, Sendable { func onEvent(_ event: any Event) async { guard let stateLayerEvent = StateLayerEvent(event: event) else { return } await sendEvent(stateLayerEvent) + + switch stateLayerEvent { + case .activityAdded(let activityData, let eventFeedId): + // Parent does not get WS update, but reposting updates its data (e.g. share count) + if let parent = activityData.parent { + await sendEvent(.activityUpdated(parent, eventFeedId)) + } + default: + break + } } } diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderList.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderList.swift index b212849..3c2f3fc 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderList.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderList.swift @@ -25,12 +25,11 @@ public final class BookmarkFolderList: Sendable { // MARK: - Paginating the List of BookmarkFolders - @discardableResult - public func get() async throws -> [BookmarkFolderData] { + @discardableResult public func get() async throws -> [BookmarkFolderData] { try await queryBookmarkFolders(with: query) } - public func queryMoreBookmarkFolders(limit: Int? = nil) async throws -> [BookmarkFolderData] { + @discardableResult public func queryMoreBookmarkFolders(limit: Int? = nil) async throws -> [BookmarkFolderData] { let nextQuery: BookmarkFoldersQuery? = await state.access { state in guard let next = state.pagination?.next else { return nil } return BookmarkFoldersQuery( diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift index 502a94e..209a8ba 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift @@ -45,12 +45,12 @@ extension BookmarkFolderListState { eventSubscription = publisher.subscribe { [weak self] event in switch event { case .bookmarkFolderDeleted(let folder): - _ = await self?.access { state in + await self?.access { state in state.folders.remove(byId: folder.id) } case .bookmarkFolderUpdated(let folder): - _ = await self?.access { state in - state.folders.replace(byId: folder) + await self?.access { state in + state.folders.sortedReplace(folder, nesting: nil, sorting: state.bookmarksSorting) } default: break @@ -58,7 +58,7 @@ extension BookmarkFolderListState { } } - func access(_ actions: @MainActor (BookmarkFolderListState) -> T) -> T { + @discardableResult func access(_ actions: @MainActor (BookmarkFolderListState) -> T) -> T { actions(self) } diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkList.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkList.swift index f5d8c74..7f62b34 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkList.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkList.swift @@ -25,12 +25,11 @@ public final class BookmarkList: Sendable { // MARK: - Paginating the List of Bookmarks - @discardableResult - public func get() async throws -> [BookmarkData] { + @discardableResult public func get() async throws -> [BookmarkData] { try await queryBookmarks(with: query) } - public func queryMoreBookmarks(limit: Int? = nil) async throws -> [BookmarkData] { + @discardableResult public func queryMoreBookmarks(limit: Int? = nil) async throws -> [BookmarkData] { let nextQuery: BookmarksQuery? = await state.access { state in guard let next = state.pagination?.next else { return nil } return BookmarksQuery( diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState.swift index c82612e..93717d9 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState.swift @@ -46,16 +46,21 @@ extension BookmarkListState { switch event { case .bookmarkFolderDeleted(let folder): await self?.access { state in - state.updateBookmarkFolder(with: folder.id, changes: { $0.folder = nil }) + state.bookmarks.updateAll( + where: { $0.folder?.id == folder.id }, + changes: { $0.folder = nil } + ) } case .bookmarkFolderUpdated(let folder): await self?.access { state in - state.updateBookmarkFolder(with: folder.id, changes: { $0.folder = folder }) + state.bookmarks.updateAll( + where: { $0.folder?.id == folder.id }, + changes: { $0.folder = folder } + ) } case .bookmarkUpdated(let bookmark): await self?.access { state in - // Only update, do not insert - state.bookmarks.replace(byId: bookmark) + state.bookmarks.sortedReplace(bookmark, nesting: nil, sorting: state.bookmarkFoldersSorting) } default: break diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedList.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedList.swift index ad29721..b9af055 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedList.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedList.swift @@ -25,12 +25,11 @@ public final class FeedList: Sendable { // MARK: - Paginating the List of Feeds - @discardableResult - public func get() async throws -> [FeedData] { + @discardableResult public func get() async throws -> [FeedData] { try await queryFeeds(with: query) } - public func queryMoreFeeds(limit: Int? = nil) async throws -> [FeedData] { + @discardableResult public func queryMoreFeeds(limit: Int? = nil) async throws -> [FeedData] { let nextQuery: FeedsQuery? = await state.access { state in guard let next = state.pagination?.next else { return nil } return FeedsQuery( diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState.swift index d6c16c9..03b189f 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState.swift @@ -42,12 +42,24 @@ import StreamCore extension FeedListState { private func subscribe(to publisher: StateLayerEventPublisher) { + let matchesQuery: @Sendable (FeedData) -> Bool = { [query] feedData in + guard let filter = query.filter else { return true } + return filter.matches(feedData) + } eventSubscription = publisher.subscribe { [weak self] event in switch event { + case .feedAdded(let feed, _): + guard matchesQuery(feed) else { return } + await self?.access { state in + state.feeds.sortedInsert(feed, sorting: state.feedsSorting) + } + case .feedDeleted(let feedId): + await self?.access { state in + state.feeds.remove(byId: feedId.rawValue) + } case .feedUpdated(let feed, _): await self?.access { state in - // Only update, do not insert - state.feeds.replace(byId: feed) + state.feeds.sortedReplace(feed, nesting: nil, sorting: state.feedsSorting) } default: break @@ -55,7 +67,7 @@ extension FeedListState { } } - func access(_ actions: @MainActor (FeedListState) -> T) -> T { + @discardableResult func access(_ actions: @MainActor (FeedListState) -> T) -> T { actions(self) } diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState.swift index 499d6f5..4ecfabb 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState.swift @@ -64,24 +64,34 @@ import StreamCore extension MemberListState { private func subscribe(to publisher: StateLayerEventPublisher) { - eventSubscription = publisher.subscribe { [weak self] event in + let matchesQuery: @Sendable (FeedMemberData) -> Bool = { [query] member in + guard let filter = query.filter else { return true } + return filter.matches(member) + } + eventSubscription = publisher.subscribe { [weak self, query] event in switch event { - case .feedMemberDeleted(let memberId, let feedId): - guard feedId == self?.query.feed else { return } + case .feedMemberAdded(let memberData, let eventFeedId): + guard eventFeedId == query.feed else { return } + guard matchesQuery(memberData) else { return } + await self?.access { state in + state.members.sortedInsert(memberData, sorting: state.membersSorting) + } + case .feedMemberDeleted(let memberId, let eventFeedId): + guard eventFeedId == query.feed else { return } await self?.access { state in - guard let index = state.members.firstIndex(where: { $0.id == memberId }) else { return } - state.members.remove(at: index) + state.members.remove(byId: memberId) } - case .feedMemberUpdated(let member, let feedId): - guard feedId == self?.query.feed else { return } + case .feedMemberUpdated(let memberData, let eventFeedId): + guard eventFeedId == query.feed else { return } await self?.access { state in - state.members.replace(byId: member) + state.members.sortedReplace(memberData, nesting: nil, sorting: state.membersSorting) } - case .feedMemberBatchUpdate(let updates, let feedId): - guard feedId == self?.query.feed else { return } + case .feedMemberBatchUpdate(let updates, let eventFeedId): + guard eventFeedId == query.feed else { return } + let added = updates.added.filter(matchesQuery) await self?.access { state in - // Skip added because the it might not belong to this list - state.members.replace(byIds: updates.updated) + added.forEach { state.members.sortedInsert($0, sorting: state.membersSorting) } + updates.updated.forEach { state.members.sortedReplace($0, nesting: nil, sorting: state.membersSorting) } state.members.remove(byIds: updates.removedIds) } default: @@ -90,7 +100,7 @@ extension MemberListState { } } - func access(_ actions: @MainActor (MemberListState) -> T) -> T { + @discardableResult func access(_ actions: @MainActor (MemberListState) -> T) -> T { actions(self) } diff --git a/StreamFeeds.xcodeproj/project.pbxproj b/StreamFeeds.xcodeproj/project.pbxproj index 74de1f4..9dc4864 100644 --- a/StreamFeeds.xcodeproj/project.pbxproj +++ b/StreamFeeds.xcodeproj/project.pbxproj @@ -858,7 +858,7 @@ repositoryURL = "https://github.com/GetStream/stream-core-swift.git"; requirement = { kind = exactVersion; - version = 0.1.0; + version = 0.2.1; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Tests/StreamFeedsTests/StateLayer/BookmarkListState_Tests.swift b/Tests/StreamFeedsTests/StateLayer/BookmarkListState_Tests.swift index 23ed4a1..13eeabc 100644 --- a/Tests/StreamFeedsTests/StateLayer/BookmarkListState_Tests.swift +++ b/Tests/StreamFeedsTests/StateLayer/BookmarkListState_Tests.swift @@ -16,7 +16,7 @@ struct BookmarkListState_Tests { let stateBookmarks = await bookmarkList.state.bookmarks #expect(bookmarks.count == 2) #expect(stateBookmarks.count == 2) - #expect(stateBookmarks.map(\.id) == ["activity-1user-1", "activity-2user-1"]) + #expect(stateBookmarks.map(\.id) == ["user-1-activity-1", "user-1-activity-2"]) #expect(bookmarks.map(\.id) == stateBookmarks.map(\.id)) await #expect(bookmarkList.state.canLoadMore == true) await #expect(bookmarkList.state.pagination?.next == "next-cursor") @@ -38,13 +38,13 @@ struct BookmarkListState_Tests { _ = try await bookmarkList.get() let initialState = await bookmarkList.state.bookmarks #expect(initialState.count == 2) - #expect(initialState.map(\.id) == ["activity-1user-1", "activity-2user-1"]) + #expect(initialState.map(\.id) == ["user-1-activity-1", "user-1-activity-2"]) // Load more let moreBookmarks = try await bookmarkList.queryMoreBookmarks() let updatedState = await bookmarkList.state.bookmarks #expect(moreBookmarks.count == 2) - #expect(moreBookmarks.map(\.id) == ["activity-3user-1", "activity-4user-1"]) + #expect(moreBookmarks.map(\.id) == ["user-1-activity-3", "user-1-activity-4"]) #expect(updatedState.count == 4) await #expect(bookmarkList.state.canLoadMore == true) await #expect(bookmarkList.state.pagination?.next == "next-cursor-2") @@ -60,7 +60,7 @@ struct BookmarkListState_Tests { _ = try await bookmarkList.get() let initialState = await bookmarkList.state.bookmarks #expect(initialState.count == 2) - #expect(initialState.first { $0.id == "activity-1user-1" }?.activity.text == "Test activity content") + #expect(initialState.first { $0.id == "user-1-activity-1" }?.activity.text == "Test activity content") // Send bookmark updated event let updatedBookmark = BookmarkResponse.dummy( @@ -75,7 +75,7 @@ struct BookmarkListState_Tests { let updatedState = await bookmarkList.state.bookmarks #expect(updatedState.count == 2) - #expect(updatedState.first { $0.id == "activity-1user-1" }?.activity.text == "Updated activity content") + #expect(updatedState.first { $0.id == "user-1-activity-1" }?.activity.text == "Updated activity content") } @Test func bookmarkFolderUpdatedEventUpdatesState() async throws { @@ -86,7 +86,7 @@ struct BookmarkListState_Tests { _ = try await bookmarkList.get() let initialState = await bookmarkList.state.bookmarks #expect(initialState.count == 2) - #expect(initialState.first { $0.id == "activity-1user-1" }?.folder?.name == "Test Folder") + #expect(initialState.first { $0.id == "user-1-activity-1" }?.folder?.name == "Test Folder") // Send bookmark folder updated event let updatedFolder = BookmarkFolderResponse.dummy(id: "folder-1", name: "Updated Folder Name").toModel() @@ -97,7 +97,7 @@ struct BookmarkListState_Tests { let updatedState = await bookmarkList.state.bookmarks #expect(updatedState.count == 2) - #expect(updatedState.first { $0.id == "activity-1user-1" }?.folder?.name == "Updated Folder Name") + #expect(updatedState.first { $0.id == "user-1-activity-1" }?.folder?.name == "Updated Folder Name") } @Test func bookmarkFolderDeletedEventUpdatesState() async throws { @@ -108,7 +108,7 @@ struct BookmarkListState_Tests { _ = try await bookmarkList.get() let initialState = await bookmarkList.state.bookmarks #expect(initialState.count == 2) - #expect(initialState.first { $0.id == "activity-1user-1" }?.folder?.id == "folder-1") + #expect(initialState.first { $0.id == "user-1-activity-1" }?.folder?.id == "folder-1") // Send bookmark folder deleted event let deletedFolder = BookmarkFolderResponse.dummy(id: "folder-1", name: "Test Folder").toModel() @@ -119,7 +119,7 @@ struct BookmarkListState_Tests { let updatedState = await bookmarkList.state.bookmarks #expect(updatedState.count == 2) - #expect(updatedState.first { $0.id == "activity-1user-1" }?.folder == nil) + #expect(updatedState.first { $0.id == "user-1-activity-1" }?.folder == nil) } // MARK: - Helper Methods diff --git a/Tests/StreamFeedsTests/StateLayer/BookmarkList_Tests.swift b/Tests/StreamFeedsTests/StateLayer/BookmarkList_Tests.swift index b9b5f4a..d55c1b2 100644 --- a/Tests/StreamFeedsTests/StateLayer/BookmarkList_Tests.swift +++ b/Tests/StreamFeedsTests/StateLayer/BookmarkList_Tests.swift @@ -16,7 +16,7 @@ struct BookmarkList_Tests { let stateBookmarks = await bookmarkList.state.bookmarks #expect(bookmarks.count == 2) #expect(stateBookmarks.count == 2) - #expect(stateBookmarks.map(\.id) == ["activity-1user-1", "activity-2user-1"]) + #expect(stateBookmarks.map(\.id) == ["user-1-activity-1", "user-1-activity-2"]) #expect(bookmarks.map(\.id) == stateBookmarks.map(\.id)) await #expect(bookmarkList.state.canLoadMore == true) await #expect(bookmarkList.state.pagination?.next == "next-cursor") @@ -38,16 +38,16 @@ struct BookmarkList_Tests { _ = try await bookmarkList.get() let initialState = await bookmarkList.state.bookmarks #expect(initialState.count == 2) - #expect(initialState.map(\.id) == ["activity-1user-1", "activity-2user-1"]) + #expect(initialState.map(\.id) == ["user-1-activity-1", "user-1-activity-2"]) // Load more let moreBookmarks = try await bookmarkList.queryMoreBookmarks() let updatedState = await bookmarkList.state.bookmarks #expect(moreBookmarks.count == 2) - #expect(moreBookmarks.map(\.id) == ["activity-3user-1", "activity-4user-1"]) + #expect(moreBookmarks.map(\.id) == ["user-1-activity-3", "user-1-activity-4"]) #expect(updatedState.count == 4) // The bookmarks should be sorted by createdAt in ascending order (oldest first) - #expect(updatedState.map(\.id) == ["activity-3user-1", "activity-4user-1", "activity-1user-1", "activity-2user-1"]) + #expect(updatedState.map(\.id) == ["user-1-activity-3", "user-1-activity-4", "user-1-activity-1", "user-1-activity-2"]) await #expect(bookmarkList.state.canLoadMore == true) await #expect(bookmarkList.state.pagination?.next == "next-cursor-2") } @@ -113,7 +113,7 @@ struct BookmarkList_Tests { // Load more with custom limit let moreBookmarks = try await bookmarkList.queryMoreBookmarks(limit: 1) #expect(moreBookmarks.count == 1) - #expect(moreBookmarks.first?.id == "activity-3user-1") + #expect(moreBookmarks.first?.id == "user-1-activity-3") } // MARK: - WebSocket Events @@ -125,7 +125,7 @@ struct BookmarkList_Tests { let initialState = await bookmarkList.state.bookmarks #expect(initialState.count == 2) - #expect(initialState.first { $0.id == "activity-1user-1" }?.activity.text == "Test activity content") + #expect(initialState.first { $0.id == "user-1-activity-1" }?.activity.text == "Test activity content") // Send bookmark updated event await client.eventsMiddleware.sendEvent( @@ -140,8 +140,8 @@ struct BookmarkList_Tests { let updatedState = await bookmarkList.state.bookmarks #expect(updatedState.count == 2) - #expect(updatedState.first { $0.id == "activity-1user-1" }?.activity.text == "Updated activity content") - #expect(updatedState.first { $0.id == "activity-2user-1" }?.activity.text == "Test activity content") + #expect(updatedState.first { $0.id == "user-1-activity-1" }?.activity.text == "Updated activity content") + #expect(updatedState.first { $0.id == "user-1-activity-2" }?.activity.text == "Test activity content") } @Test func bookmarkFolderUpdatedEventUpdatesState() async throws { @@ -151,7 +151,7 @@ struct BookmarkList_Tests { let initialState = await bookmarkList.state.bookmarks #expect(initialState.count == 2) - #expect(initialState.first { $0.id == "activity-1user-1" }?.folder?.name == "Test Folder") + #expect(initialState.first { $0.id == "user-1-activity-1" }?.folder?.name == "Test Folder") // Send bookmark folder updated event await client.eventsMiddleware.sendEvent( @@ -162,8 +162,7 @@ struct BookmarkList_Tests { let updatedState = await bookmarkList.state.bookmarks #expect(updatedState.count == 2) - #expect(updatedState.first { $0.id == "activity-1user-1" }?.folder?.name == "Updated Folder Name") - #expect(updatedState.first { $0.id == "activity-2user-1" }?.folder?.name == "Test Folder") + #expect(updatedState.map(\.folder?.name) == ["Updated Folder Name", "Updated Folder Name"], "Same folder") } @Test func bookmarkFolderDeletedEventUpdatesState() async throws { @@ -173,7 +172,7 @@ struct BookmarkList_Tests { let initialState = await bookmarkList.state.bookmarks #expect(initialState.count == 2) - #expect(initialState.first { $0.id == "activity-1user-1" }?.folder?.name == "Test Folder") + #expect(initialState.first { $0.id == "user-1-activity-1" }?.folder?.name == "Test Folder") // Send bookmark folder deleted event await client.eventsMiddleware.sendEvent( @@ -184,8 +183,7 @@ struct BookmarkList_Tests { let updatedState = await bookmarkList.state.bookmarks #expect(updatedState.count == 2) - #expect(updatedState.first { $0.id == "activity-1user-1" }?.folder == nil) - #expect(updatedState.first { $0.id == "activity-2user-1" }?.folder?.name == "Test Folder") + #expect(updatedState.map(\.folder) == [nil, nil], "Same folder") } @Test func bookmarkUpdatedEventForUnrelatedUserDoesNotUpdateState() async throws { @@ -195,7 +193,7 @@ struct BookmarkList_Tests { let initialState = await bookmarkList.state.bookmarks #expect(initialState.count == 2) - #expect(initialState.first { $0.id == "activity-1user-1" }?.activity.text == "Test activity content") + #expect(initialState.first { $0.id == "user-1-activity-1" }?.activity.text == "Test activity content") // Send bookmark updated event for unrelated user await client.eventsMiddleware.sendEvent( @@ -210,8 +208,8 @@ struct BookmarkList_Tests { let updatedState = await bookmarkList.state.bookmarks #expect(updatedState.count == 2) - #expect(updatedState.first { $0.id == "activity-1user-1" }?.activity.text == "Test activity content") - #expect(updatedState.first { $0.id == "activity-2user-1" }?.activity.text == "Test activity content") + #expect(updatedState.first { $0.id == "user-1-activity-1" }?.activity.text == "Test activity content") + #expect(updatedState.first { $0.id == "user-1-activity-2" }?.activity.text == "Test activity content") } // MARK: - Helper Methods diff --git a/Tests/StreamFeedsTests/StateLayer/CommentReactionList_Tests.swift b/Tests/StreamFeedsTests/StateLayer/CommentReactionList_Tests.swift index ea20f5b..9255bc0 100644 --- a/Tests/StreamFeedsTests/StateLayer/CommentReactionList_Tests.swift +++ b/Tests/StreamFeedsTests/StateLayer/CommentReactionList_Tests.swift @@ -16,8 +16,8 @@ struct CommentReactionList_Tests { ) let reactions = try await reactionList.get() let stateReactions = await reactionList.state.reactions - #expect(reactions.map(\.id) == ["current-user-id-like-activity-123"]) - #expect(stateReactions.map(\.id) == ["current-user-id-like-activity-123"]) + #expect(reactions.map(\.id) == ["current-user-id-like-comment-123-activity-123"]) + #expect(stateReactions.map(\.id) == ["current-user-id-like-comment-123-activity-123"]) } @Test func paginationLoadsMoreReactions() async throws { @@ -42,17 +42,17 @@ struct CommentReactionList_Tests { ) // Initial load - #expect(try await reactionList.get().map(\.id) == ["current-user-id-like-activity-123"]) + #expect(try await reactionList.get().map(\.id) == ["current-user-id-like-comment-123-activity-123"]) #expect(await reactionList.state.canLoadMore == true) // Load more let moreReactions = try await reactionList.queryMoreReactions() - #expect(moreReactions.map(\.id) == ["other-user-heart-activity-123"]) + #expect(moreReactions.map(\.id) == ["other-user-heart-comment-123-activity-123"]) #expect(await reactionList.state.canLoadMore == false) // Check final state let finalStateReactions = await reactionList.state.reactions - #expect(finalStateReactions.map(\.id) == ["current-user-id-like-activity-123", "other-user-heart-activity-123"], "Newest first") + #expect(finalStateReactions.map(\.id) == ["current-user-id-like-comment-123-activity-123", "other-user-heart-comment-123-activity-123"], "Newest first") } @Test func commentReactionAddedEventUpdatesState() async throws { @@ -75,7 +75,7 @@ struct CommentReactionList_Tests { ) let result = await reactionList.state.reactions.map(\.id) - #expect(result == ["other-user-heart-activity-123", "current-user-id-like-activity-123"]) + #expect(result == ["other-user-heart-comment-123-activity-123", "current-user-id-like-comment-123-activity-123"]) let reactions = await reactionList.state.reactions #expect(reactions.map(\.type) == ["heart", "like"]) #expect(reactions.map(\.user.id) == ["other-user", "current-user-id"]) @@ -97,7 +97,7 @@ struct CommentReactionList_Tests { ) let result = await reactionList.state.reactions.map(\.id) - #expect(result == ["current-user-id-like-activity-123"]) + #expect(result == ["current-user-id-like-comment-123-activity-123"]) let updatedReaction = await reactionList.state.reactions.first #expect(updatedReaction?.custom == ["key": RawJSON.string("UPDATED")]) } @@ -137,7 +137,7 @@ struct CommentReactionList_Tests { ) let result = await reactionList.state.reactions.map(\.id) - #expect(result == ["current-user-id-like-activity-123"]) // Should not be affected + #expect(result == ["current-user-id-like-comment-123-activity-123"]) // Should not be affected } @Test func addedEventsOnlyAffectMatchingComment() async throws { @@ -156,7 +156,7 @@ struct CommentReactionList_Tests { ) let result = await reactionList.state.reactions.map(\.id) - #expect(result == ["current-user-id-like-activity-123"]) + #expect(result == ["current-user-id-like-comment-123-activity-123"]) let reaction = await reactionList.state.reactions.first #expect(reaction?.type == "like") // Should remain unchanged } @@ -177,7 +177,7 @@ struct CommentReactionList_Tests { ) let result = await reactionList.state.reactions.map(\.id) - #expect(result == ["current-user-id-like-activity-123"]) + #expect(result == ["current-user-id-like-comment-123-activity-123"]) let reaction = await reactionList.state.reactions.first #expect(reaction?.type == "like") } diff --git a/Tests/StreamFeedsTests/TestTools/BookmarkFolderData+Testing.swift b/Tests/StreamFeedsTests/TestTools/BookmarkFolderData+Testing.swift deleted file mode 100644 index 0594074..0000000 --- a/Tests/StreamFeedsTests/TestTools/BookmarkFolderData+Testing.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -import StreamCore -@testable import StreamFeeds - -extension BookmarkFolderData { - static func dummy( - id: String = "folder-1", - name: String = "Test Folder", - createdAt: Date = .fixed(), - updatedAt: Date = .fixed(), - custom: [String: RawJSON]? = nil - ) -> BookmarkFolderData { - BookmarkFolderData( - createdAt: createdAt, - custom: custom, - id: id, - name: name, - updatedAt: updatedAt - ) - } -} diff --git a/Tests/StreamFeedsTests/TestTools/FeedData+Testing.swift b/Tests/StreamFeedsTests/TestTools/FeedData+Testing.swift deleted file mode 100644 index b1982bb..0000000 --- a/Tests/StreamFeedsTests/TestTools/FeedData+Testing.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -import StreamCore -@testable import StreamFeeds - -extension FeedData { - static func dummy( - id: String = "feed-1", - name: String = "Test Feed", - createdAt: Date = .fixed(), - updatedAt: Date = .fixed(), - createdBy: UserData = .dummy(id: "user-1"), - feed: FeedId = FeedId(rawValue: "user:feed-1"), - custom: [String: RawJSON]? = nil, - deletedAt: Date? = nil, - description: String = "Test feed description", - filterTags: [String]? = nil, - followerCount: Int = 0, - followingCount: Int = 0, - groupId: String = "user", - memberCount: Int = 0, - pinCount: Int = 0, - visibility: String? = nil, - ownCapabilities: [FeedOwnCapability]? = nil - ) -> FeedData { - FeedData( - createdAt: createdAt, - createdBy: createdBy, - custom: custom, - deletedAt: deletedAt, - description: description, - feed: feed, - filterTags: filterTags, - followerCount: followerCount, - followingCount: followingCount, - groupId: groupId, - id: id, - memberCount: memberCount, - name: name, - pinCount: pinCount, - updatedAt: updatedAt, - visibility: visibility, - ownCapabilities: ownCapabilities - ) - } -} diff --git a/Tests/StreamFeedsTests/TestTools/FeedMemberData+Testing.swift b/Tests/StreamFeedsTests/TestTools/FeedMemberData+Testing.swift deleted file mode 100644 index 9fbeba0..0000000 --- a/Tests/StreamFeedsTests/TestTools/FeedMemberData+Testing.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -import StreamCore -@testable import StreamFeeds - -extension FeedMemberData { - static func dummy( - id: String = "member-1", - user: UserData = .dummy(id: "user-1"), - createdAt: Date = .fixed(), - updatedAt: Date = .fixed(), - custom: [String: RawJSON]? = nil, - inviteAcceptedAt: Date? = nil, - inviteRejectedAt: Date? = nil, - role: String = "member", - status: FeedMemberStatus = .member - ) -> FeedMemberData { - FeedMemberData( - createdAt: createdAt, - custom: custom, - inviteAcceptedAt: inviteAcceptedAt, - inviteRejectedAt: inviteRejectedAt, - role: role, - status: status, - updatedAt: updatedAt, - user: user - ) - } -} diff --git a/Tests/StreamFeedsTests/TestTools/UserData+Testing.swift b/Tests/StreamFeedsTests/TestTools/UserData+Testing.swift deleted file mode 100644 index 4abf1b9..0000000 --- a/Tests/StreamFeedsTests/TestTools/UserData+Testing.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -import StreamCore -@testable import StreamFeeds - -extension UserData { - static func dummy( - id: String = "user-1", - name: String? = "Test User", - image: String? = nil, - createdAt: Date = .fixed(), - updatedAt: Date = .fixed(), - banned: Bool = false, - blockedUserIds: [String] = [], - custom: [String: RawJSON] = [:], - deactivatedAt: Date? = nil, - deletedAt: Date? = nil, - language: String = "en", - lastActive: Date? = nil, - online: Bool = false, - revokeTokensIssuedBefore: Date? = nil, - role: String = "user", - teams: [String] = [], - teamsRole: [String: String]? = nil - ) -> UserData { - UserData( - banned: banned, - blockedUserIds: blockedUserIds, - createdAt: createdAt, - custom: custom, - deactivatedAt: deactivatedAt, - deletedAt: deletedAt, - id: id, - image: image, - language: language, - lastActive: lastActive, - name: name, - online: online, - revokeTokensIssuedBefore: revokeTokensIssuedBefore, - role: role, - teams: teams, - teamsRole: teamsRole, - updatedAt: updatedAt - ) - } -}