Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FilterKey.members and support for equal operator to match an array #3536

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add `ChannelListSortingKey.pinnedAt`
- Add `ChatChannel.membership.pinnedAt`
- Add `ChatChannel.isPinned`
- Add channel list filtering key: `FilterKey.members` [#3536](https://github.com/GetStream/stream-chat-swift/pull/3536)
- Add member list filtering keys: `FilterKey.channelRole` and `FilterKey.email` [#3535](https://github.com/GetStream/stream-chat-swift/pull/3535)
- Add member list sorting key: `ChannelMemberListSortingKey.channelRole` [#3535](https://github.com/GetStream/stream-chat-swift/pull/3535)
### 🐞 Fixed
Expand Down
19 changes: 18 additions & 1 deletion DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ final class DemoChatChannelListVC: ChatChannelListVC {
.containMembers(userIds: [currentUserId]),
.equal(.pinned, to: true)
]))

lazy var equalMembersQuery: ChannelListQuery = .init(filter:
.equal(.members, values: [currentUserId, "r2-d2"])
)

var demoRouter: DemoChatChannelListRouter? {
router as? DemoChatChannelListRouter
Expand Down Expand Up @@ -170,6 +174,14 @@ final class DemoChatChannelListVC: ChatChannelListVC {
self?.title = "Pinned Channels"
self?.setPinnedChannelsQuery()
}

let equalMembersAction = UIAlertAction(
title: "R2-D2 Channels (Equal Members)",
style: .default
) { [weak self] _ in
self?.title = "R2-D2 Channels (Equal Members)"
self?.setEqualMembersChannelsQuery()
}

presentAlert(
title: "Filter Channels",
Expand All @@ -180,7 +192,8 @@ final class DemoChatChannelListVC: ChatChannelListVC {
mutedChannelsAction,
coolChannelsAction,
pinnedChannelsAction,
archivedChannelsAction
archivedChannelsAction,
equalMembersAction
].sorted(by: { $0.title ?? "" < $1.title ?? "" }),
preferredStyle: .actionSheet,
sourceView: filterChannelsButton
Expand Down Expand Up @@ -216,6 +229,10 @@ final class DemoChatChannelListVC: ChatChannelListVC {
func setPinnedChannelsQuery() {
replaceQuery(pinnedChannelsQuery)
}

func setEqualMembersChannelsQuery() {
replaceQuery(equalMembersQuery)
}

func setInitialChannelsQuery() {
replaceQuery(initialQuery)
Expand Down
23 changes: 12 additions & 11 deletions Sources/StreamChat/Query/ChannelListQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,6 @@ extension Filter where Scope: AnyChannelListFilterScope {
}
}

// We don't want to expose `members` publicly because it can't be used with any other operator
// than `$in`. We expose it publicly via the `containMembers` filter helper.
Comment on lines -49 to -50
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we expose something like equalMembers(userIds:) since only $eq and $in operator are supported?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It feels more closer to public docs if we allow using it directly. But I am lacking some historic knowledge on channel list filters so not sure what is best here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With other keys we can create incompatible combinations.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, my guess is that for some properties, we started limiting the operators, especially for performance reasons. But for other properties we did not follow the same approach.

IMO, it would be better that programmatically you could only use the operators that you are allowed to, and that it would not require you to look into docs to see which operators are supported. It would be better to just limit every field but now it is kinda too late, since we already expose most of them :/

So yeah, maybe it is better we open up this one for consistency

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we have that containMembers, maybe it is better then to have equalMembers. Hmm

extension FilterKey where Scope: AnyChannelListFilterScope {
static var members: FilterKey<Scope, UserId> { .init(rawValue: "members", keyPathString: #keyPath(ChannelDTO.members.user.id)) }
}

/// Filter values to be used with `.invite` FilterKey.
public enum InviteFilterValue: String, FilterValue {
case pending
Expand Down Expand Up @@ -88,12 +82,13 @@ public extension FilterKey where Scope: AnyChannelListFilterScope {
/// A filter key for matching the `createdBy` value.
/// Supported operators: `equal`
static var createdBy: FilterKey<Scope, UserId> { .init(rawValue: "created_by_id", keyPathString: #keyPath(ChannelDTO.createdBy.id)) }

/// A filter key for matching the `createdAt` value.
/// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`, `notEqual`
/// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`, `exists`
static var createdAt: FilterKey<Scope, Date> { .init(rawValue: "created_at", keyPathString: #keyPath(ChannelDTO.createdAt)) }

/// A filter key for matching the `updatedAt` value.
/// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`, `notEqual`
/// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`
static var updatedAt: FilterKey<Scope, Date> { .init(rawValue: "updated_at", keyPathString: #keyPath(ChannelDTO.updatedAt)) }

/// A filter key for matching the `deletedAt` value.
Expand All @@ -114,6 +109,7 @@ public extension FilterKey where Scope: AnyChannelListFilterScope {
static var blocked: FilterKey<Scope, Bool> { .init(rawValue: "blocked", keyPathString: #keyPath(ChannelDTO.isBlocked)) }

/// A filter key for matching the `archived` value.
/// Supported operators: `equal`
static var archived: FilterKey<Scope, Bool> {
.init(
rawValue: "archived",
Expand All @@ -131,6 +127,7 @@ public extension FilterKey where Scope: AnyChannelListFilterScope {
}

/// A filter key for matching the `pinned` value.
/// Supported operators: `equal`
static var pinned: FilterKey<Scope, Bool> {
.init(
rawValue: "pinned",
Expand All @@ -147,8 +144,12 @@ public extension FilterKey where Scope: AnyChannelListFilterScope {
)
}

/// A filter key for matching channel members.
/// Supported operators: `in`, `equal`
static var members: FilterKey<Scope, UserId> { .init(rawValue: "members", keyPathString: #keyPath(ChannelDTO.members.user.id)) }

/// A filter key for matching the `memberCount` value.
/// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`, `notEqual`
/// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`
static var memberCount: FilterKey<Scope, Int> { .init(rawValue: "member_count", keyPathString: #keyPath(ChannelDTO.memberCount)) }

/// A filter key for matching the `team` value.
Expand Down Expand Up @@ -192,12 +193,12 @@ public extension FilterKey where Scope: AnyChannelListFilterScope {
static var invite: FilterKey<Scope, InviteFilterValue> { "invite" }

/// Filter for checking the `name` property of a user who is a member of the channel
/// Supported operators: `equal`, `notEqual`, `autocomplete`
/// Supported operators: `equal`, `autocomplete`
/// - Warning: This filter is considerably expensive for the backend so avoid using this when possible.
static var memberName: FilterKey<Scope, String> { .init(rawValue: "member.user.name", keyPathString: #keyPath(ChannelDTO.members.user.name), isCollectionFilter: true) }

/// Filter for the time of the last message in the channel. If the channel has no messages, then the time the channel was created.
/// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`, `notEqual`
/// Supported operators: `equal`, `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`
static var lastUpdatedAt: FilterKey<Scope, Date> { .init(rawValue: "last_updated", keyPathString: #keyPath(ChannelDTO.lastMessageAt)) }
}

Expand Down
21 changes: 20 additions & 1 deletion Sources/StreamChat/Query/Filter+ChatChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@ extension Filter where Scope == ChannelListFilterScope {
}

switch op {
case .equal, .notEqual, .greater, .greaterOrEqual, .less, .lessOrEqual:
case .equal:
if mappedValue is [FilterValue] {
return collectionPredicate(op)
} else {
return comparingPredicate(op)
}
case .notEqual, .greater, .greaterOrEqual, .less, .lessOrEqual:
return comparingPredicate(op)
case .in, .notIn, .autocomplete, .contains, .exists:
return collectionPredicate(op)
Expand Down Expand Up @@ -164,6 +170,19 @@ extension Filter where Scope == ChannelListFilterScope {
}
)

case .equal where mappedValue is [FilterValue]:
guard let filterArray = mappedArrayValue else {
return nil
}
return NSCompoundPredicate(
andPredicateWithSubpredicates: filterArray.map { subValue in
NSPredicate(
format: "%@ IN %K",
argumentArray: [subValue, keyPathString]
)
}
)

case .notIn where mappedValue is [FilterValue]:
guard let filterArray = mappedArrayValue else {
return nil
Expand Down
13 changes: 12 additions & 1 deletion Sources/StreamChat/Query/Filter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation

/// An enum with possible operators to use in filters.
public enum FilterOperator: String {
/// Matches values that are equal to a specified value.
/// Matches values that are equal to a specified value or matches all of the values in an array.
case equal = "$eq"

/// Matches all values that are not equal to a specified value.
Expand Down Expand Up @@ -299,6 +299,17 @@ public extension Filter {
keyPathString: key.keyPathString
)
}

/// Matches values that are equal to a specified values.
static func equal<Value: Encodable>(_ key: FilterKey<Scope, Value>, values: [Value]) -> Filter {
.init(
operator: .equal,
key: key,
value: values,
valueMapper: key.valueMapper,
keyPathString: key.keyPathString
)
}

/// Matches all values that are not equal to a specified value.
@available(*, deprecated, message: "The notEqual filter will be removed in the future")
Expand Down
10 changes: 10 additions & 0 deletions Tests/StreamChatTests/Query/Filter_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ final class Filter_Tests: XCTestCase {
XCTAssertEqual(filter.key, FilterKey<FilterTestScope, String>.testKey.rawValue)
XCTAssertEqual(filter.value as? String, "equal value")
XCTAssertEqual(filter.operator, FilterOperator.equal.rawValue)

filter = .equal(.testKey, values: ["eq value 1", "eq value 2"])
XCTAssertEqual(filter.key, FilterKey<FilterTestScope, String>.testKey.rawValue)
XCTAssertEqual(filter.value as? [String], ["eq value 1", "eq value 2"])
XCTAssertEqual(filter.operator, FilterOperator.equal.rawValue)

filter = .notEqual(.testKey, to: "not equal value")
XCTAssertEqual(filter.key, FilterKey<FilterTestScope, String>.testKey.rawValue)
Expand Down Expand Up @@ -93,6 +98,11 @@ final class Filter_Tests: XCTestCase {
filter = .init(operator: FilterOperator.in.rawValue, key: "test_key", value: [1, 2, 3], isCollectionFilter: false)
XCTAssertEqual(filter.serialized, #"{"test_key":{"$in":[1,2,3]}}"#)
XCTAssertEqual(jsonString.deserializeFilter(), filter)

// Test eq values filter
filter = .init(operator: FilterOperator.equal.rawValue, key: "test_key", value: [1, 2, 3], isCollectionFilter: false)
XCTAssertEqual(filter.serialized, #"{"test_key":{"$eq":[1,2,3]}}"#)
XCTAssertEqual(jsonString.deserializeFilter(), filter)

// Test group filter
let filter1: Filter<FilterTestScope> = .equal(.testKey, to: "test_value_1")
Expand Down
Loading