Skip to content
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 .github/workflows/smoke-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
- run: bundle exec fastlane rubocop
- run: bundle exec fastlane run_swift_format strict:true
- run: bundle exec fastlane validate_public_interface
- run: bundle exec fastlane validate_generated_code
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”₯


build-old-xcode:
name: Build SDKs (Old Xcode)
Expand Down
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
excluded:
- Scripts
- Sources/StreamChat/Generated
- Sources/StreamChatCommonUI/Generated
- Sources/StreamChatUI/StreamSwiftyGif
- Sources/StreamChatUI/StreamNuke
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## StreamChat
### βœ… Added
- Add `ChannelListQuery(predefinedFilter:filterValues:sortValues:)` for creating channel list queries with predefined filters [#4113](https://github.com/GetStream/stream-chat-swift/pull/4113)
- Add `ChatClient.queryGroupedChannels(groups:limit:presence:watch:)` to fetch grouped channels with per-group unread counts [#4076](https://github.com/GetStream/stream-chat-swift/pull/4076)
- Add `ChatClient.makeChannelList(with:)` overload for observing a single grouped channels group in the state layer [#4076](https://github.com/GetStream/stream-chat-swift/pull/4076)
- Add `unreadChannelCountsByGroup` to `CurrentChatUser`, observable for changes via `ConnectedUser` [#4076](https://github.com/GetStream/stream-chat-swift/pull/4076)
Expand Down
70 changes: 70 additions & 0 deletions DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,34 @@ final class DemoChatChannelListVC: ChatChannelListVC {

lazy var premiumTaggedChannelsQuery: ChannelListQuery = .init(filter: .in(.filterTags, values: ["premium"]))

lazy var predefinedMessagingChannelsQuery: ChannelListQuery = .init(
predefinedFilter: "user_per_channel_type_channels",
filterValues: ["channel_type": .string(ChannelType.messaging.rawValue), "user_id": .string(currentUserId)],
sortValues: nil
)

lazy var predefinedArchivedChannelsQuery: ChannelListQuery = .init(
predefinedFilter: "user_per_channel_type_archived_hidden",
filterValues: [
"channel_type": .string(ChannelType.messaging.rawValue),
"hidden": .bool(false),
"user_id": .string(currentUserId),
"archived": .bool(true)
],
sortValues: nil
)

lazy var predefinedHiddenChannelsQuery: ChannelListQuery = .init(
predefinedFilter: "user_per_channel_type_archived_hidden",
filterValues: [
"channel_type": .string(ChannelType.messaging.rawValue),
"hidden": .bool(true),
"user_id": .string(currentUserId),
"archived": .bool(false)
],
sortValues: nil
)

lazy var livestreamChannelsQuery: ChannelListQuery = .init(filter: .equal(.type, to: .livestream))

var demoRouter: DemoChatChannelListRouter? {
Expand Down Expand Up @@ -270,6 +298,33 @@ final class DemoChatChannelListVC: ChatChannelListVC {
}
)

let predefinedMessagingChannelsAction = UIAlertAction(
title: "Messaging (Predefined)",
style: .default,
handler: { [weak self] _ in
self?.title = "Messaging (Predefined)"
self?.setPredefinedMessagingChannelsQuery()
}
)

let predefinedArchivedChannelsAction = UIAlertAction(
title: "Archived (Predefined)",
style: .default,
handler: { [weak self] _ in
self?.title = "Archived (Predefined)"
self?.setPredefinedArchivedChannelsQuery()
}
)

let predefinedHiddenChannelsAction = UIAlertAction(
title: "Hidden (Predefined)",
style: .default,
handler: { [weak self] _ in
self?.title = "Hidden (Predefined)"
self?.setPredefinedHiddenChannelsQuery()
}
)

let livestreamChannelsAction = UIAlertAction(
title: "Livestream Channels",
style: .default
Expand All @@ -294,6 +349,9 @@ final class DemoChatChannelListVC: ChatChannelListVC {
equalMembersAction,
channelRoleChannelsAction,
taggedChannelsAction,
predefinedMessagingChannelsAction,
predefinedArchivedChannelsAction,
predefinedHiddenChannelsAction,
livestreamChannelsAction
].sorted(by: { $0.title ?? "" < $1.title ?? "" }),
preferredStyle: .actionSheet,
Expand Down Expand Up @@ -355,6 +413,18 @@ final class DemoChatChannelListVC: ChatChannelListVC {
replaceQuery(premiumTaggedChannelsQuery)
}

func setPredefinedMessagingChannelsQuery() {
replaceQuery(predefinedMessagingChannelsQuery)
}

func setPredefinedArchivedChannelsQuery() {
replaceQuery(predefinedArchivedChannelsQuery)
}

func setPredefinedHiddenChannelsQuery() {
replaceQuery(predefinedHiddenChannelsQuery)
}

func setLivestreamChannelsQuery() {
replaceQuery(livestreamChannelsQuery)
}
Expand Down
1 change: 1 addition & 0 deletions Githubfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export INTERFACE_ANALYZER_VERSION='1.0.7'
export SWIFT_LINT_VERSION='0.59.1'
export SWIFT_FORMAT_VERSION='0.58.2'
export SWIFT_GEN_VERSION='6.5.1'
export SOURCERY_VERSION='2.3.0'
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ MAKEFLAGS += --silent
bootstrap:
./Scripts/bootstrap.sh

generate:
sourcery --config Sources/StreamChat/.sourcery.yml

update_dependencies:
echo "πŸ‘‰ Updating Nuke"
make update_nuke version=10.3.3
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ let package = Package(
dependencies: [
.product(name: "StreamCore", package: "stream-core-swift")
],
exclude: ["Info.plist"],
exclude: ["Info.plist", "Generated/PredefinedFilter.stencil"],
resources: [.copy("Database/StreamChatModel.xcdatamodeld")]
),
.target(
Expand Down
13 changes: 13 additions & 0 deletions Scripts/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ if [ "${SKIP_SWIFT_BOOTSTRAP:-}" != true ]; then
sudo sudo rm -f "$BIN_PATH"
sudo sudo ln -s "$INSTALL_DIR/bin/swiftgen" "$BIN_PATH"
swiftgen --version

puts "Install Sourcery v${SOURCERY_VERSION}"
DOWNLOAD_URL="https://github.com/krzysztofzablocki/Sourcery/releases/download/${SOURCERY_VERSION}/sourcery-${SOURCERY_VERSION}.zip"
DOWNLOAD_PATH="/tmp/sourcery-${SOURCERY_VERSION}.zip"
INSTALL_DIR="/usr/local/lib/sourcery"
BIN_PATH="/usr/local/bin/sourcery"
wget "$DOWNLOAD_URL" -O "$DOWNLOAD_PATH"
sudo rm -rf "$INSTALL_DIR"
sudo mkdir -p "$INSTALL_DIR"
sudo unzip -o "$DOWNLOAD_PATH" -d "$INSTALL_DIR"
sudo rm -f "$BIN_PATH"
sudo ln -s "$INSTALL_DIR/bin/sourcery" "$BIN_PATH"
sourcery --version
fi

if [[ ${INSTALL_SONAR-default} == true ]]; then
Expand Down
7 changes: 7 additions & 0 deletions Sources/StreamChat/.sourcery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
sources:
- ./Query/ChannelListQuery.swift
- ./Query/Sorting/ChannelListSortingKey.swift
templates:
- ./Generated/PredefinedFilter.stencil
output:
./Generated/PredefinedFilter+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,48 @@ import Foundation
struct ChannelListPayload {
/// A list of channels response (see `ChannelQuery`).
let channels: [ChannelPayload]

/// Server-resolved predefined filter, present only when the query was made with a predefined filter.
let predefinedFilter: PredefinedFilterPayload?

init(channels: [ChannelPayload], predefinedFilter: PredefinedFilterPayload? = nil) {
self.channels = channels
self.predefinedFilter = predefinedFilter
}
}

extension ChannelListPayload: Decodable {
enum CodingKeys: String, CodingKey {
case channels
case predefinedFilter = "predefined_filter"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let channels = try container
.decodeArrayIgnoringFailures([ChannelPayload].self, forKey: .channels)
let predefinedFilter = try container
.decodeIfPresent(PredefinedFilterPayload.self, forKey: .predefinedFilter)

self.init(
channels: channels
channels: channels,
predefinedFilter: predefinedFilter
)
}
}

final class PredefinedFilterPayload: Decodable, Sendable {
let name: String
let filter: [String: RawJSON]
Comment thread
martinmitrevski marked this conversation as resolved.
let sort: [[String: RawJSON]]

init(name: String, filter: [String: RawJSON], sort: [[String: RawJSON]]) {
self.name = name
self.filter = filter
self.sort = sort
}
}

final class GroupedQueryChannelsRequestBody: Encodable, Sendable {
let limit: Int?
let groups: [String: GroupedQueryChannelsRequestGroup]?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ extension ChatClient {
/// - Note: For an async-await alternative of the `ChatChannelListController`, please check ``ChannelList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
public class ChatChannelListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable {
/// The query specifying and filtering the list of channels.
public let query: ChannelListQuery
public internal(set) var query: ChannelListQuery

/// The `ChatClient` instance this controller belongs to.
public let client: ChatClient
Expand Down Expand Up @@ -82,8 +82,15 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
}

private(set) lazy var channelListObserver: BackgroundListDatabaseObserver<ChatChannel, ChannelDTO> = {
if let updated = worker.loadPredefinedFilter(for: query) {
query = updated
}
Comment on lines +85 to +87
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For supporting app relaunches where API call has not finished and we load the filter from CoreData and pass it to FRC.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had 2 options here:
a) load it async, but then FRC needs to be recreated/reloaded anyway (2 times, one without correct local filter, second with loaded filter) (this is because only CoreData knows the fetches filter JSON)
b) block, load filter, create FRC once

return makeChannelListObserver()
}()

private func makeChannelListObserver() -> BackgroundListDatabaseObserver<ChatChannel, ChannelDTO> {
let request = ChannelDTO.channelListFetchRequest(query: self.query, chatClientConfig: client.config)
let observer = self.environment.createChannelListDatabaseObserver(
let observer = environment.createChannelListDatabaseObserver(
client.databaseContainer,
request,
{ try $0.asModel() },
Expand All @@ -101,7 +108,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
}
}
return observer
}()
}

var _basePublishers: Any?
/// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose
Expand Down Expand Up @@ -146,11 +153,8 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
startChannelListObserverIfNeeded()
channelListLinker.start(with: client.eventNotificationCenter)
client.syncRepository.startTrackingChannelListController(self)
updateChannelList { [weak self] error in
guard let completion else { return }
self?.callback {
completion(error)
}
updateChannelList { [weak self] result in
self?.callback { completion?(result.error) }
}
}

Expand Down Expand Up @@ -179,9 +183,9 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
updatedQuery.pagination = Pagination(pageSize: limit, offset: channels.count)
worker.update(channelListQuery: updatedQuery) { result in
switch result {
case let .success(channels):
self.markChannelsAsDeliveredIfNeeded(channels: channels)
self.hasLoadedAllPreviousChannels = channels.count < limit
case let .success(updateResult):
self.markChannelsAsDeliveredIfNeeded(channels: updateResult.channels)
self.hasLoadedAllPreviousChannels = updateResult.channels.count < limit
self.callback { completion?(nil) }
case let .failure(error):
self.callback { completion?(error) }
Expand All @@ -199,24 +203,30 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
// MARK: - Helpers

private func updateChannelList(
_ completion: (@MainActor (_ error: Error?) -> Void)? = nil
_ completion: (@MainActor (Result<ChannelListUpdateResult, Error>) -> Void)? = nil
) {
let limit = query.pagination.pageSize
worker.update(
channelListQuery: query
) { [weak self] result in
switch result {
case let .success(channels):
case let .success(updateResult):
self?.state = .remoteDataFetched
self?.hasLoadedAllPreviousChannels = channels.count < limit
self?.hasLoadedAllPreviousChannels = updateResult.channels.count < limit

// Mark channels as delivered if synchronization was successful
self?.markChannelsAsDeliveredIfNeeded(channels: channels)
self?.markChannelsAsDeliveredIfNeeded(channels: updateResult.channels)

// Predefined filters can update local query representation (query gets backend defined filter and sort which must be set to FRC)
if let updatedQuery = updateResult.updatedQuery {
self?.query = updatedQuery
self?.updateChannelListObserver()
}

self?.callback { completion?(nil) }
self?.callback { completion?(.success(updateResult)) }
case let .failure(error):
self?.state = .remoteDataFetchFailed(ClientError(with: error))
self?.callback { completion?(error) }
self?.callback { completion?(.failure(error)) }
}
}
}
Expand Down Expand Up @@ -264,6 +274,16 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
log.error("Failed to perform fetch request with error: \(error). This is an internal error.")
}
}

private func updateChannelListObserver() {
channelListObserver = makeChannelListObserver()
do {
try channelListObserver.startObserving()
} catch {
state = .localDataFetchFailed(ClientError(with: error))
log.error("Failed to update the channel list observer: \(error)")
}
}
}

extension ChatChannelListController {
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChat/Database/DTOs/ChannelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ extension NSManagedObjectContext {
// the query won't be saved, which will cause any future
// channels to not become linked to this query
if let query = query {
_ = saveQuery(query: query)
_ = saveQuery(query: query, predefinedFilter: payload.predefinedFilter)
}

return payload.channels.compactMapLoggingError { channelPayload in
Expand Down
Loading
Loading