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 support for channel member extra data #3487

Merged
merged 25 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6e7b9ec
Add premium icon to DemoApp's User Cell
nuno-vieira Oct 23, 2024
5685774
Add `ChatChannelMemberController.partialUpdate()` to partially update…
nuno-vieira Oct 23, 2024
e1abec4
Change `ChatChannelController.addMembers()` to support additional mem…
nuno-vieira Oct 23, 2024
86d48a5
Add channel member premium feature to the Demo App
nuno-vieira Oct 23, 2024
2a2ee6b
Add memberExtraData to ChatMember
nuno-vieira Nov 12, 2024
f5f10c0
Add a way to query members with extra data
nuno-vieira Nov 12, 2024
491de96
Fix test compilation
nuno-vieira Nov 12, 2024
acb8f5f
Add test coverage
nuno-vieira Nov 12, 2024
b90bee3
Update CHANGELOG.md
nuno-vieira Nov 12, 2024
f8720ba
Merge branch 'develop' into add/channel-members-extra-data
nuno-vieira Nov 13, 2024
c0f14dc
Update gitignore to include VSCode / Cursor
nuno-vieira Nov 14, 2024
964be7a
Add `CurrentUserController.updateMemberData()`
nuno-vieira Nov 14, 2024
b243982
Make unsetProperties optional
nuno-vieira Nov 14, 2024
8637662
Only show Premium member feature in the Demo App if enabled
nuno-vieira Nov 14, 2024
978b9ec
Make the DemoApp-StreamDevelopers scheme accessible
nuno-vieira Nov 14, 2024
c8676cf
Make MemberInfo.extraData optional
nuno-vieira Nov 14, 2024
920ddce
Do not deprecate the previous method
nuno-vieira Nov 14, 2024
d69c130
Update CHANGELOG.md
nuno-vieira Nov 14, 2024
334f4ca
Updating current user member extra data does not require capability
nuno-vieira Nov 14, 2024
357fed2
Update CHANGELOG.md
nuno-vieira Nov 14, 2024
0dab83f
Fix AddMemberInput typo
nuno-vieira Nov 14, 2024
2779b47
Do not show premium badge on the demo app if feature is disabled
nuno-vieira Nov 14, 2024
074cba8
Merge branch 'develop' into add/channel-members-extra-data
testableapple Nov 18, 2024
ab4bc50
Add unset premium member action in Demo App
nuno-vieira Nov 18, 2024
e993758
Merge branch 'develop' into add/channel-members-extra-data
nuno-vieira Nov 18, 2024
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
# Upcoming

## StreamChat
### βœ… Added
- Add support for channel member extra data [#3487](https://github.com/GetStream/stream-chat-swift/pull/3487)
- Add `ChatChannelMemberController.partialUpdate(extraData:unsetProperties:)` [#3487](https://github.com/GetStream/stream-chat-swift/pull/3487)
- Add `ChatChannelController.addMembers(_ members: [MemberInfo])` [#3487](https://github.com/GetStream/stream-chat-swift/pull/3487)
- Exposes `ChatChannelMember.memberExtraData` property [#3487](https://github.com/GetStream/stream-chat-swift/pull/3487)
### 🐞 Fixed
- Fix connection not resuming after guest user goes to background [#3483](https://github.com/GetStream/stream-chat-swift/pull/3483)
- Fix empty channel list if the channel list filter contains OR statement with only custom filtering keys [#3482](https://github.com/GetStream/stream-chat-swift/pull/3482)
### πŸ”„ Changed
- Deprecates `ChatChannelController.addMembers(userIds: [UserId])` [#3487](https://github.com/GetStream/stream-chat-swift/pull/3487)

# [4.66.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.66.0)
_November 05, 2024_
Expand Down
47 changes: 23 additions & 24 deletions DemoApp/Screens/Create Chat/NameGroupViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import Foundation
import Nuke
import StreamChat
import StreamChatUI
import UIKit

class NameGroupViewController: UIViewController {
Expand All @@ -14,6 +15,7 @@ class NameGroupViewController: UIViewController {
let avatarView = AvatarView()
let nameLabel = UILabel()
let removeButton = UIButton()
let premiumImageView = UIImageView(image: .init(systemName: "crown.fill")!)

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
Expand All @@ -26,31 +28,27 @@ class NameGroupViewController: UIViewController {
}

func setupUI() {
[avatarView, nameLabel, removeButton].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview($0)
}

removeButton.tintColor = .black
removeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
removeButton.tintColor = Appearance.default.colorPalette.text
removeButton.setImage(UIImage(systemName: "xmark")!, for: .normal)
removeButton.imageView?.contentMode = .scaleAspectFit

NSLayoutConstraint.activate([
// AvatarView
avatarView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0),
avatarView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: contentView.layoutMargins.top),
avatarView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -contentView.layoutMargins.bottom),
avatarView.heightAnchor.constraint(equalToConstant: 40),
avatarView.widthAnchor.constraint(equalTo: avatarView.heightAnchor),
// NameLabel
nameLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: contentView.layoutMargins.left),
nameLabel.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
// removeButton
removeButton.leftAnchor.constraint(equalTo: nameLabel.rightAnchor, constant: contentView.layoutMargins.left),
removeButton.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -contentView.layoutMargins.right),
removeButton.widthAnchor.constraint(equalToConstant: 10),
removeButton.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor)
])
removeButton.isUserInteractionEnabled = true
premiumImageView.contentMode = .scaleAspectFill
premiumImageView.tintColor = .systemBlue
premiumImageView.isHidden = true

HContainer(spacing: 8, alignment: .center) {
avatarView
.width(30)
.height(30)
nameLabel
Spacer()
premiumImageView
.width(20)
.height(20)
removeButton
.width(30)
.height(30)
}.embedToMargins(in: contentView)
}
}

Expand Down Expand Up @@ -154,6 +152,7 @@ extension NameGroupViewController: UITableViewDataSource {
}

cell.nameLabel.text = user.name
cell.premiumImageView.isHidden = true
cell.removeButton.addAction(.init(handler: { [weak self] _ in
guard let self = self else { return }
if let index = self.selectedUsers.firstIndex(of: user) {
Expand Down
7 changes: 7 additions & 0 deletions DemoApp/Screens/MembersViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class MembersViewController: UITableViewController, ChatChannelMemberListControl
}
cell.nameLabel.text = member.name ?? member.id
cell.removeButton.isHidden = true
cell.premiumImageView.isHidden = member.isPremium == false
return cell
}

Expand All @@ -69,3 +70,9 @@ class MembersViewController: UITableViewController, ChatChannelMemberListControl
updateData()
}
}

extension ChatChannelMember {
var isPremium: Bool {
memberExtraData["is_premium"]?.boolValue == true
}
}
52 changes: 50 additions & 2 deletions DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
return
}
channelController.addMembers(
userIds: [id],
[MemberInfo(userId: id, extraData: nil)],
message: "Members added to the channel"
) { error in
if let error = error {
Expand All @@ -184,7 +184,7 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
return
}
channelController.addMembers(
userIds: [id],
[MemberInfo(userId: id, extraData: nil)],
hideHistory: true,
message: "Members added to the channel"
) { error in
Expand All @@ -197,6 +197,45 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
}
}
}),
.init(title: "Add premium member", isEnabled: canUpdateChannelMembers, handler: { [unowned self] _ in
self.rootViewController.presentAlert(title: "Enter user id", textFieldPlaceholder: "User ID") { id in
guard let id = id, !id.isEmpty else {
self.rootViewController.presentAlert(title: "User ID is not valid")
return
}
channelController.addMembers(
[MemberInfo(userId: id, extraData: ["is_premium": true])],
message: "Premium member added to the channel"
) { error in
if let error = error {
self.rootViewController.presentAlert(
title: "Couldn't add user \(id) to channel \(cid)",
message: "\(error)"
)
}
}
}
}),
.init(title: "Set member as premium", isEnabled: canUpdateChannelMembers, handler: { [unowned self] _ in
let actions = channelController.channel?.lastActiveMembers.map { member in
UIAlertAction(title: member.id, style: .default) { _ in
channelController.client.memberController(userId: member.id, in: cid)
.partialUpdate(extraData: ["is_premium": true], unsetProperties: nil) { [unowned self] result in
do {
let data = try result.get()
print("Member updated. Premium: ", data.isPremium)
self.rootNavigationController?.popViewController(animated: true)
} catch {
self.rootViewController.presentAlert(
title: "Couldn't set user \(member.id) as premium.",
message: "\(error)"
)
}
}
}
} ?? []
self.rootViewController.presentAlert(title: "Select a member", actions: actions)
}),
.init(title: "Remove a member", isEnabled: canUpdateChannelMembers, handler: { [unowned self] _ in
let actions = channelController.channel?.lastActiveMembers.map { member in
UIAlertAction(title: member.id, style: .default) { _ in
Expand Down Expand Up @@ -418,6 +457,15 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
membersController: client.memberListController(query: .init(cid: cid, pageSize: 105))
), animated: true)
}),
.init(title: "Show Channel Premium Members", handler: { [unowned self] _ in
guard let cid = channelController.channel?.cid else { return }
let client = channelController.client
self.rootViewController.present(MembersViewController(
membersController: client.memberListController(
query: .init(cid: cid, filter: .equal("is_premium", to: true), pageSize: 105)
)
), animated: true)
}),
.init(title: "Show Channel Moderators", handler: { [unowned self] _ in
guard let cid = channelController.channel?.cid else { return }
let client = channelController.client
Expand Down
20 changes: 18 additions & 2 deletions Sources/StreamChat/APIClient/Endpoints/ChannelEndpoints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,12 @@ extension Endpoint {

static func addMembers(
cid: ChannelId,
userIds: Set<UserId>,
members: [MemberInfoRequest],
hideHistory: Bool,
messagePayload: MessageRequestBody? = nil
) -> Endpoint<EmptyResponse> {
var body: [String: AnyEncodable] = [
"add_members": AnyEncodable(userIds),
"add_members": AnyEncodable(members),
"hide_history": AnyEncodable(hideHistory)
]
if let messagePayload = messagePayload {
Expand Down Expand Up @@ -363,3 +363,19 @@ extension Endpoint {
)
}
}

struct MemberInfoRequest: Encodable {
let userId: UserId
let extraData: [String: RawJSON]?

enum CodingKeys: String, CodingKey {
case userId = "user_id"
case extraData
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(userId, forKey: .userId)
try extraData?.encode(to: encoder)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ extension EndpointPath {
switch self {
case .sendMessage, .editMessage, .deleteMessage, .pinMessage, .unpinMessage, .addReaction, .deleteReaction:
return true
case .createChannel, .connect, .sync, .users, .guest, .members, .search, .devices, .channels, .updateChannel,
case .createChannel, .connect, .sync, .users, .guest, .members, .partialMemberUpdate, .search, .devices, .channels, .updateChannel,
.deleteChannel, .channelUpdate, .muteChannel, .showChannel, .truncateChannel, .markChannelRead, .markChannelUnread,
.markAllChannelsRead, .channelEvent, .stopWatchingChannel, .pinnedMessages, .uploadAttachment, .message,
.replies, .reactions, .messageAction, .banMember, .flagUser, .flagMessage, .muteUser, .translateMessage,
.callToken, .createCall, .deleteFile, .deleteImage, .og, .appSettings, .threads, .thread, .markThreadRead, .markThreadUnread, .polls, .pollsQuery,
.poll, .pollOption, .pollOptions, .pollVotes, .pollVoteInMessage, .pollVote, .unread, .blockUser, .unblockUser:
.callToken, .createCall, .deleteFile, .deleteImage, .og, .appSettings, .threads, .thread, .markThreadRead, .markThreadUnread,
.polls, .pollsQuery, .poll, .pollOption, .pollOptions, .pollVotes, .pollVoteInMessage, .pollVote,
.unread, .blockUser, .unblockUser:
return false
}
}
Expand Down
9 changes: 7 additions & 2 deletions Sources/StreamChat/APIClient/Endpoints/EndpointPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ enum EndpointPath: Codable {
case sync
case users
case guest
case members
case search
case devices
case og
case unread

case members
case partialMemberUpdate(userId: UserId, cid: ChannelId)

case threads
case thread(messageId: MessageId)
case markThreadRead(cid: ChannelId)
Expand Down Expand Up @@ -79,12 +81,15 @@ enum EndpointPath: Codable {
case .sync: return "sync"
case .users: return "users"
case .guest: return "guest"
case .members: return "members"
case .search: return "search"
case .devices: return "devices"
case .og: return "og"
case .unread: return "unread"

case .members: return "members"
case let .partialMemberUpdate(userId, cid):
return "channels/\(cid.apiPath)/member/\(userId)"

case .threads:
return "threads"
case let .thread(threadId):
Expand Down
31 changes: 31 additions & 0 deletions Sources/StreamChat/APIClient/Endpoints/MemberEndpoints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,35 @@ extension Endpoint {
body: ["payload": query]
)
}

static func partialMemberUpdate(
userId: UserId,
cid: ChannelId,
extraData: [String: RawJSON]?,
unset: [String]?
) -> Endpoint<PartialMemberUpdateResponse> {
var body: [String: AnyEncodable] = [:]
if let extraData {
body["set"] = AnyEncodable(extraData)
}
if let unset {
body["unset"] = AnyEncodable(unset)
}

return .init(
path: .partialMemberUpdate(userId: userId, cid: cid),
method: .patch,
queryItems: nil,
requiresConnectionId: false,
body: body
)
}
}

struct PartialMemberUpdateResponse: Decodable {
var channelMember: MemberPayload

enum CodingKeys: String, CodingKey {
case channelMember = "channel_member"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct MemberContainerPayload: Decodable {
}

struct MemberPayload: Decodable {
private enum CodingKeys: String, CodingKey {
private enum CodingKeys: String, CodingKey, CaseIterable {
case user
case userId = "user_id"
case role = "channel_role"
Expand Down Expand Up @@ -67,6 +67,9 @@ struct MemberPayload: Decodable {
/// A boolean value that returns whether the user has muted the channel or not.
let notificationsMuted: Bool

/// Extra data associated with the member.
let extraData: [String: RawJSON]?

init(
user: UserPayload?,
userId: String,
Expand All @@ -79,7 +82,8 @@ struct MemberPayload: Decodable {
isInvited: Bool? = nil,
inviteAcceptedAt: Date? = nil,
inviteRejectedAt: Date? = nil,
notificationsMuted: Bool = false
notificationsMuted: Bool = false,
extraData: [String: RawJSON]? = nil
) {
self.user = user
self.userId = userId
Expand All @@ -93,6 +97,7 @@ struct MemberPayload: Decodable {
self.inviteAcceptedAt = inviteAcceptedAt
self.inviteRejectedAt = inviteRejectedAt
self.notificationsMuted = notificationsMuted
self.extraData = extraData
}

init(from decoder: Decoder) throws {
Expand All @@ -114,6 +119,14 @@ struct MemberPayload: Decodable {
} else {
userId = try container.decode(String.self, forKey: .userId)
}

do {
var payload = try [String: RawJSON](from: decoder)
payload.removeValues(forKeys: CodingKeys.allCases.map(\.rawValue))
extraData = payload
} catch {
extraData = [:]
}
}
}

Expand Down
Loading
Loading