This commit is contained in:
启星
2025-08-08 10:49:36 +08:00
parent 6400cf78bb
commit b5ce3d580a
8780 changed files with 978183 additions and 0 deletions

View File

@@ -0,0 +1,322 @@
//
// UserListCell.swift
// TUIRoomKit
//
// Created by janejntang on 2024/5/7.
//
import Foundation
import RTCRoomEngine
import Factory
import Combine
class UserListCell: UITableViewCell {
var attendeeModel: UserEntity
var viewModel: UserListViewModel
let avatarImageView: UIImageView = {
let img = UIImageView()
img.layer.cornerRadius = 20
img.layer.masksToBounds = true
return img
}()
let userLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xD5E0F2)
label.backgroundColor = UIColor.clear
label.textAlignment = isRTL ? .right : .left
label.textAlignment = .left
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
label.numberOfLines = 1
return label
}()
let roleImageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
let roleLabel: UILabel = {
let label = UILabel ()
label.font = UIFont(name: "PingFangSC-Regular", size: 12)
label.backgroundColor = UIColor.clear
label.textColor = UIColor(0x4791FF)
return label
}()
let muteAudioButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "room_unMute_audio", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn(), for: .normal)
button.setImage(UIImage(named: "room_mute_audio_red", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn(), for: .selected)
return button
}()
let muteVideoButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "room_unMute_video", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn(), for: .normal)
button.setImage(UIImage(named: "room_mute_video_red", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn(), for: .selected)
return button
}()
let inviteStageButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = UIColor(0x0565FA)
button.layer.cornerRadius = 6
button.setTitle(.inviteSeatText, for: .normal)
button.setTitleColor(UIColor(0xFFFFFF), for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .regular)
button.isHidden = true
return button
}()
let callButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = UIColor(0x6B758A)
button.layer.cornerRadius = 6
button.setTitle(.callText, for: .normal)
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 12)
button.isHidden = true
return button
}()
let callingLabel: UILabel = {
let label = UILabel ()
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
label.backgroundColor = UIColor.clear
label.textColor = UIColor(0xD5E0F2)
label.text = .callingText
label.isHidden = true
return label
}()
let notJoiningLabel: UILabel = {
let label = UILabel ()
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
label.backgroundColor = UIColor.clear
label.textAlignment = .right
label.textColor = UIColor(0xD5E0F2)
label.text = .notJoinNowText
label.isHidden = true
return label
}()
let downLineView : UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x4F586B,alpha: 0.3)
return view
}()
init(attendeeModel: UserEntity ,viewModel: UserListViewModel) {
self.attendeeModel = attendeeModel
self.viewModel = viewModel
super.init(style: .default, reuseIdentifier: "UserListCell")
}
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
isViewReady = true
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func constructViewHierarchy() {
contentView.addSubview(avatarImageView)
contentView.addSubview(userLabel)
contentView.addSubview(roleImageView)
contentView.addSubview(roleLabel)
contentView.addSubview(muteAudioButton)
contentView.addSubview(muteVideoButton)
contentView.addSubview(inviteStageButton)
contentView.addSubview(downLineView)
contentView.addSubview(callButton)
contentView.addSubview(callingLabel)
contentView.addSubview(notJoiningLabel)
}
func activateConstraints() {
avatarImageView.snp.makeConstraints { make in
make.width.height.equalTo(40)
make.leading.equalToSuperview()
make.centerY.equalToSuperview()
}
muteVideoButton.snp.makeConstraints { make in
make.width.height.equalTo(20.scale375())
make.trailing.equalToSuperview()
make.centerY.equalTo(self.avatarImageView)
}
muteAudioButton.snp.makeConstraints { make in
make.width.height.equalTo(20.scale375())
make.trailing.equalTo(self.muteVideoButton.snp.leading).offset(-20.scale375())
make.centerY.equalTo(self.avatarImageView)
}
inviteStageButton.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalTo(self.avatarImageView)
make.width.equalTo(80.scale375())
make.height.equalTo(30.scale375Height())
}
userLabel.snp.makeConstraints { make in
if attendeeModel.userRole == .generalUser {
make.centerY.equalToSuperview()
} else {
make.top.equalToSuperview().offset(10.scale375Height())
}
make.leading.equalTo(avatarImageView.snp.trailing).offset(12.scale375())
make.width.equalTo(150.scale375())
make.height.equalTo(22.scale375())
}
roleImageView.snp.makeConstraints { make in
make.top.equalTo(userLabel.snp.bottom).offset(2.scale375Height())
make.leading.equalTo(avatarImageView.snp.trailing).offset(12.scale375())
make.width.height.equalTo(14.scale375())
}
roleLabel.snp.makeConstraints { make in
make.centerY.equalTo(roleImageView)
make.leading.equalTo(roleImageView.snp.trailing).offset(2.scale375())
make.trailing.equalTo(81.scale375())
make.height.equalTo(16.scale375())
}
callButton.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalTo(self.avatarImageView)
make.width.equalTo(48.scale375())
make.height.equalTo(28.scale375Height())
}
callingLabel.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalTo(self.avatarImageView)
make.width.equalTo(60.scale375())
make.height.equalTo(28.scale375Height())
}
notJoiningLabel.snp.makeConstraints{ make in
make.trailing.equalTo(callButton.snp.leading).offset(-12.scale375())
make.centerY.equalTo(self.avatarImageView)
make.width.equalTo(120.scale375())
make.height.equalTo(28.scale375Height())
}
downLineView.snp.makeConstraints { make in
make.leading.equalTo(userLabel)
make.trailing.equalToSuperview()
make.bottom.equalToSuperview()
make.height.equalTo(1.scale375())
}
}
func bindInteraction() {
backgroundColor = UIColor(0x17181F)
setupViewState(item: attendeeModel)
inviteStageButton.addTarget(self, action: #selector(inviteStageAction(sender:)), for: .touchUpInside)
muteAudioButton.addTarget(self, action: #selector(showUserManageAction(sender:)), for: .touchUpInside)
muteVideoButton.addTarget(self, action: #selector(showUserManageAction(sender:)), for: .touchUpInside)
callButton.addTarget(self, action: #selector(inviteEnterAction(sender:)), for: .touchUpInside)
}
func setupViewState(item: UserEntity) {
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
if let url = URL(string: item.avatarUrl) {
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
} else {
avatarImageView.image = placeholder
}
if item.userId == viewModel.currentUser.userId {
userLabel.text = item.userName + "" + .meText + ""
} else {
userLabel.text = item.userName
}
switch item.userRole {
case .roomOwner:
roleImageView.image = UIImage(named: "room_role_owner", in: tuiRoomKitBundle(), compatibleWith: nil)
roleLabel.text = .ownerText
case .administrator:
roleImageView.image = UIImage(named: "room_role_administrator", in: tuiRoomKitBundle(), compatibleWith: nil)
roleLabel.text = .administratorText
default: break
}
roleImageView.isHidden = item.userRole == .generalUser
roleLabel.isHidden = item.userRole == .generalUser
muteAudioButton.isSelected = !item.hasAudioStream
muteVideoButton.isSelected = !item.hasVideoStream
if viewModel.roomInfo.isSeatEnabled {
muteAudioButton.isHidden = !attendeeModel.isOnSeat
muteVideoButton.isHidden = !attendeeModel.isOnSeat
if viewModel.checkSelfInviteAbility(invitee: attendeeModel) {
inviteStageButton.isHidden = attendeeModel.isOnSeat
} else {
inviteStageButton.isHidden = true
}
}
setupCallingViewState(item: item)
}
private func setupCallingViewState(item: UserEntity) {
if let index = viewModel.invitationList.firstIndex(where: { $0.invitee.userId == item.userId }) {
let invitation = viewModel.invitationList[index]
muteAudioButton.isHidden = true
muteVideoButton.isHidden = true
inviteStageButton.isHidden = true
let isPending = invitation.status == .pending
callButton.isHidden = isPending
callingLabel.isHidden = !isPending
notJoiningLabel.isHidden = true
}
}
func showNotJoiningLabel() {
self.notJoiningLabel.isHidden = false
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.notJoiningLabel.isHidden = true
}
}
@objc func inviteStageAction(sender: UIButton) {
viewModel.userId = attendeeModel.userId
viewModel.inviteSeatAction(sender: sender)
}
@objc func showUserManageAction(sender: UIButton) {
viewModel.showUserManageViewAction(userId: attendeeModel.userId, userName: attendeeModel.userName)
}
@objc func inviteEnterAction(sender: UIButton) {
self.conferenceStore.dispatch(action: ConferenceInvitationActions.inviteUsers(payload: (viewModel.roomInfo.roomId, [attendeeModel.userId])))
}
deinit {
debugPrint("deinit \(self)")
}
@Injected(\.conferenceStore) private var conferenceStore
}
private extension String {
static var inviteSeatText: String {
localized("Invite to stage")
}
static var meText: String {
localized("Me")
}
static var ownerText: String {
localized("Host")
}
static var administratorText: String {
localized("Administrator")
}
static var callText: String {
localized("Call")
}
static var callingText: String {
localized("Calling...")
}
static var notJoinNowText: String {
localized("Not joining for now")
}
}

View File

@@ -0,0 +1,316 @@
//
// UserListManagerView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/5.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
class UserListManagerView: UIView {
var viewModel: UserListManagerViewModel
private var isViewReady: Bool = false
private var viewArray: [ButtonItemView] = []
private var currentLandscape: Bool = isLandscape
let contentView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = UIColor(0x22262E)
view.layer.cornerRadius = 12
return view
}()
let dropArrowButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "room_drop_arrow", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
button.contentEdgeInsets = UIEdgeInsets(top: 12.scale375Height(), left: 20.scale375(), bottom: 12.scale375Height(), right: 20.scale375())
return button
}()
let rightArrowButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "room_right_arrow", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
button.contentEdgeInsets = UIEdgeInsets(top: 20.scale375Height(), left: 12.scale375(), bottom: 20.scale375Height(), right: 12.scale375())
return button
}()
let scrollView: UIScrollView = {
let view = UIScrollView()
return view
}()
let avatarImageView: UIImageView = {
let img = UIImageView()
img.layer.cornerRadius = 20
img.layer.masksToBounds = true
return img
}()
let userLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xD5E0F2)
label.textAlignment = isRTL ? .right : .left
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
label.numberOfLines = 1
return label
}()
let headView: UIView = {
let view = UIView()
return view
}()
let stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.alignment = .center
view.distribution = .equalSpacing
view.spacing = 0
return view
}()
let backBlockView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x17181F)
view.alpha = 0.9
return view
}()
init(viewModel: UserListManagerViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
alpha = 0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
override func layoutSubviews() {
super.layoutSubviews()
guard currentLandscape != isLandscape else { return }
setupViewOrientation(isLandscape: isLandscape)
currentLandscape = isLandscape
}
private func constructViewHierarchy() {
addSubview(backBlockView)
addSubview(contentView)
contentView.addSubview(dropArrowButton)
contentView.addSubview(rightArrowButton)
contentView.addSubview(scrollView)
scrollView.addSubview(headView)
scrollView.addSubview(stackView)
headView.addSubview(avatarImageView)
headView.addSubview(userLabel)
setupStackView()
}
private func activateConstraints() {
setupViewOrientation(isLandscape: isLandscape)
headView.snp.makeConstraints { make in
make.top.leading.equalToSuperview()
make.trailing.equalToSuperview().offset(-16.scale375())
make.height.equalTo(40.scale375())
}
avatarImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalToSuperview()
make.width.height.equalTo(40.scale375())
}
userLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalTo(avatarImageView.snp.trailing).offset(10.scale375())
make.trailing.equalToSuperview()
make.height.equalTo(22.scale375())
}
stackView.snp.makeConstraints { make in
make.top.equalTo(headView.snp.bottom).offset(20.scale375())
make.leading.trailing.equalToSuperview()
make.bottom.equalToSuperview()
make.width.equalToSuperview()
}
}
private func setupViewOrientation(isLandscape: Bool) {
backBlockView.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
contentView.snp.remakeConstraints { make in
if isLandscape {
make.height.equalToSuperview()
} else {
make.height.equalTo(500.scale375())
}
make.bottom.leading.trailing.equalToSuperview()
}
dropArrowButton.snp.remakeConstraints { make in
make.height.equalTo(isLandscape ? 0 : 43.scale375())
make.top.centerX.equalToSuperview()
}
rightArrowButton.snp.remakeConstraints { make in
make.width.equalTo(isLandscape ? 27.scale375() : 0)
make.leading.centerY.equalToSuperview()
}
scrollView.snp.remakeConstraints { make in
make.top.equalTo(dropArrowButton.snp.bottom).offset(10.scale375())
if isLandscape {
make.leading.equalTo(rightArrowButton.snp.trailing).offset(5.scale375())
} else {
make.leading.equalToSuperview().offset(16.scale375())
}
make.bottom.equalToSuperview()
make.trailing.equalToSuperview().offset(-16.scale375())
}
}
func bindInteraction() {
viewModel.viewResponder = self
setupViewState()
let tap = UITapGestureRecognizer(target: self, action: #selector(dismiss))
backBlockView.addGestureRecognizer(tap)
dropArrowButton.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
rightArrowButton.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
}
private func setupStackView() {
for item in viewModel.userListManagerItems {
let view = ButtonItemView(itemData: item)
viewArray.append(view)
stackView.addArrangedSubview(view)
view.snp.makeConstraints { make in
make.height.equalTo(53.scale375())
make.width.equalToSuperview()
}
}
}
private func setupViewState() {
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
guard let attendeeModel = viewModel.attendeeList.first(where: { $0.userId == viewModel.selectUserId }) else { return }
if let url = URL(string: attendeeModel.avatarUrl) {
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
} else {
avatarImageView.image = placeholder
}
if attendeeModel.userId == viewModel.currentUser.userId {
userLabel.text = attendeeModel.userName + "(" + .meText + ")"
} else {
userLabel.text = attendeeModel.userName
}
}
func updateStackView(items:[ButtonItemData]) {
for view in viewArray {
view.removeFromSuperview()
}
viewArray.removeAll()
for item in items {
let view = ButtonItemView(itemData: item)
viewArray.append(view)
stackView.addArrangedSubview(view)
view.snp.makeConstraints { make in
make.height.equalTo(53.scale375())
make.width.equalToSuperview()
}
}
}
func show(rootView: UIView) {
rootView.addSubview(self)
self.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
setupViewOrientation(isLandscape: isLandscape)
UIView.animate(withDuration: 0.3) { [weak self] in
guard let self = self else { return }
self.alpha = 1
self.contentView.transform = .identity
}
}
@objc func dismiss() {
UIView.animate(withDuration: 0.3) { [weak self] in
guard let self = self else { return }
self.alpha = 0
self.contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
} completion: { [weak self] _ in
guard let self = self else { return }
self.removeFromSuperview()
}
}
@objc func backBlockAction(sender: RoomInfoView) {
viewModel.backBlockAction(sender: self)
}
deinit {
debugPrint("deinit \(self)")
}
}
extension UserListManagerView: UserListManagerViewEventResponder {
func showAlert(title: String?, message: String?, sureTitle: String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) {
RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock)
}
func updateUI(item: ButtonItemData) {
guard let view = viewArray.first(where: { $0.itemData.buttonType == item.buttonType }) else { return }
view.setupViewState(item: item)
}
func addStackView(item: ButtonItemData, index: Int?) {
let view = ButtonItemView(itemData: item)
if let index = index, viewArray.count > index + 1 {
viewArray.insert(view, at: index)
stackView.insertArrangedSubview(view, at: index)
} else {
viewArray.append(view)
stackView.addArrangedSubview(view)
}
view.snp.makeConstraints { make in
make.height.equalTo(53.scale375())
make.width.equalToSuperview()
}
}
func removeStackView(itemType: ButtonItemData.ButtonType) {
let views = viewArray.filter({ view in
view.itemData.buttonType == itemType
})
views.forEach { view in
view.removeFromSuperview()
}
viewArray.removeAll(where: { $0.itemData.buttonType == itemType })
}
func dismissView() {
dismiss()
}
func makeToast(text : String) {
RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
}
func setUserListManagerViewHidden(isHidden: Bool) {
self.isHidden = true
}
}
private extension String {
static var meText: String {
localized("Me")
}
}

View File

@@ -0,0 +1,631 @@
//
// UserListView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/4.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import Factory
import Combine
import RTCRoomEngine
class UserListView: UIView {
let viewModel: UserListViewModel
let memberLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xD5E0F2)
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
return label
}()
let searchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.placeholder = .searchMemberText
searchBar.setBackgroundImage(UIColor(0x17181F).trans2Image(), for: .top, barMetrics: .default)
if #available(iOS 13, *) {
searchBar.searchTextField.textColor = UIColor(0xB2BBD1)
searchBar.searchTextField.tintColor = UIColor(0xB2BBD1).withAlphaComponent(0.3)
searchBar.searchTextField.layer.cornerRadius = 6
searchBar.searchTextField.font = UIFont.systemFont(ofSize: 14, weight: .medium)
} else {
if let textField = searchBar.value(forKey: "searchField") as? UITextField {
textField.textColor = UIColor(0xB2BBD1)
textField.tintColor = UIColor(0xB2BBD1).withAlphaComponent(0.3)
textField.layer.cornerRadius = 6
textField.font = UIFont.systemFont(ofSize: 14, weight: .medium)
}
}
return searchBar
}()
let searchControl: UIControl = {
let view = UIControl()
view.backgroundColor = .clear
view.isHidden = true
return view
}()
let listStateView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = UIColor(0x4F586B).withAlphaComponent(0.3)
view.layer.cornerRadius = 6
return view
}()
let haveEnteredButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitleColor(UIColor(0xD5E0F2), for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 14)
button.setBackgroundImage(UIColor(0xD5E0F2).withAlphaComponent(0.3).trans2Image(), for: .selected)
button.setBackgroundImage(UIColor.clear.trans2Image(), for: .normal)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.isSelected = true
return button
}()
let onStageButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitleColor(UIColor(0xD5E0F2), for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 14)
button.setBackgroundImage(UIColor(0xD5E0F2).withAlphaComponent(0.3).trans2Image(), for: .selected)
button.setBackgroundImage(UIColor.clear.trans2Image(), for: .normal)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.isSelected = true
return button
}()
let offStageButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitleColor(UIColor(0xD5E0F2), for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 14)
button.setBackgroundImage(UIColor(0xD5E0F2).withAlphaComponent(0.3).trans2Image(), for: .selected)
button.setBackgroundImage(UIColor.clear.trans2Image(), for: .normal)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.isSelected = false
return button
}()
let invatationListButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitleColor(UIColor(0xD5E0F2), for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 14)
button.setBackgroundImage(UIColor(0xD5E0F2).withAlphaComponent(0.3).trans2Image(), for: .selected)
button.setBackgroundImage(UIColor.clear.trans2Image(), for: .normal)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.isSelected = false
return button
}()
let muteAllAudioButton: UIButton = {
let button = UIButton(type: .custom)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
button.setTitle(.allMuteAudioText, for: .normal)
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
button.setTitle(.allUnMuteAudioText, for: .selected)
button.setTitleColor(UIColor(0xF2504B), for: .selected)
button.setTitle(.allUnMuteAudioText, for: [.selected, .highlighted])
button.setTitleColor(UIColor(0xF2504B), for: [.selected, .highlighted])
button.backgroundColor = UIColor(0x4F586B, alpha: 0.3)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.titleLabel?.adjustsFontSizeToFitWidth = true
return button
}()
let muteAllVideoButton: UIButton = {
let button = UIButton(type: .custom)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
button.setTitle(.allMuteVideoText, for: .normal)
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
button.setTitle(.allUnMuteVideoText, for: .selected)
button.setTitleColor(UIColor(0xF2504B), for: .selected)
button.setTitle(.allUnMuteVideoText, for: [.selected, .highlighted])
button.setTitleColor(UIColor(0xF2504B), for: [.selected, .highlighted])
button.backgroundColor = UIColor(0x4F586B, alpha: 0.3)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.titleLabel?.adjustsFontSizeToFitWidth = true
return button
}()
let moreFunctionButton: UIButton = {
let button = UIButton(type: .custom)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
button.setTitle(.moreText, for: .normal)
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
button.backgroundColor = UIColor(0x4F586B, alpha: 0.3)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.titleLabel?.adjustsFontSizeToFitWidth = true
return button
}()
let callEveryoneButton: UIButton = {
let button = UIButton(type: .custom)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
button.setTitle(.callEveryoneText, for: .normal)
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
button.backgroundColor = UIColor(0x4F586B)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.isHidden = true
return button
}()
let bottomControlView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x17181F)
return view
}()
lazy var userListTableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = UIColor(0x17181F)
tableView.register(UserListCell.self, forCellReuseIdentifier: "UserListCell")
return tableView
}()
lazy var raiseHandNotificationView: RaiseHandApplicationNotificationView = {
let viewModel = RaiseHandApplicationNotificationViewModel()
viewModel.delayDisappearanceTime = 0
let applicationNotificationView = RaiseHandApplicationNotificationView(viewModel: viewModel)
applicationNotificationView.delegate = self.viewModel
return applicationNotificationView
}()
private lazy var invitationListPublisher = {
conferenceStore.select(ConferenceInvitationSelectors.getInvitationList)
}()
var cancellableSet = Set<AnyCancellable>()
init(viewModel: UserListViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
isViewReady = true
backgroundColor = UIColor(0x17181F)
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
func constructViewHierarchy() {
addSubview(memberLabel)
addSubview(searchBar)
addSubview(listStateView)
listStateView.addSubview(haveEnteredButton)
listStateView.addSubview(onStageButton)
listStateView.addSubview(offStageButton)
listStateView.addSubview(invatationListButton)
addSubview(raiseHandNotificationView)
addSubview(userListTableView)
addSubview(bottomControlView)
bottomControlView.addSubview(muteAllAudioButton)
bottomControlView.addSubview(muteAllVideoButton)
bottomControlView.addSubview(moreFunctionButton)
addSubview(callEveryoneButton)
addSubview(searchControl)
}
func activateConstraints() {
memberLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10.scale375Height())
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.height.equalTo(24.scale375Height())
}
searchBar.snp.makeConstraints { make in
make.top.equalTo(memberLabel.snp.bottom).offset(18.scale375Height())
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.height.equalTo(36.scale375Height())
}
if #available(iOS 13.0, *) {
searchBar.searchTextField.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
} else {
if let searchField = searchBar.value(forKey: "searchField") as? UITextField {
searchField.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
listStateView.snp.makeConstraints { make in
make.top.equalTo(searchBar.snp.bottom).offset(15.scale375Height())
make.leading.trailing.equalTo(searchBar)
make.height.equalTo(36.scale375Height())
}
setupListStateView()
bottomControlView.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.height.equalTo(84.scale375Height())
}
callEveryoneButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.bottom.equalToSuperview().offset(-34.scale375())
make.height.equalTo(40.scale375Height())
}
raiseHandNotificationView.snp.makeConstraints { make in
make.top.equalTo(listStateView.snp.bottom).offset(10.scale375Height())
make.leading.equalToSuperview().offset(8.scale375())
make.trailing.equalToSuperview().offset(-8.scale375())
make.height.equalTo(40.scale375Height())
}
setupUserListTableView()
muteAllAudioButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10.scale375Height())
make.leading.equalToSuperview().offset(16.scale375())
make.width.equalTo(108.scale375())
make.height.equalTo(40.scale375())
}
muteAllVideoButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10.scale375Height())
make.leading.equalToSuperview().offset(133.scale375())
make.width.equalTo(108.scale375())
make.height.equalTo(40.scale375())
}
moreFunctionButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10.scale375Height())
make.leading.equalToSuperview().offset(250.scale375())
make.width.equalTo(108.scale375())
make.height.equalTo(40.scale375())
}
searchControl.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func setupUserListTableView() {
guard userListTableView.superview != nil else { return }
userListTableView.snp.remakeConstraints { make in
let aboveView = viewModel.isShownNotificationView ? raiseHandNotificationView : listStateView
let bottomView = viewModel.userListType == .notInRoomUsers ? callEveryoneButton : bottomControlView
make.top.equalTo(aboveView.snp.bottom).offset(15.scale375Height())
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.bottom.equalTo(bottomView.snp.top)
}
}
private func setupListStateView() {
if viewModel.isSeatEnabled {
onStageButton.snp.remakeConstraints { make in
make.left.centerY.equalToSuperview()
make.height.equalTo(32.scale375Height())
make.width.equalToSuperview().multipliedBy(1.0 / 3.0)
}
offStageButton.snp.remakeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalTo(onStageButton.snp.trailing)
make.height.equalTo(32.scale375Height())
make.width.equalToSuperview().multipliedBy(1.0 / 3.0)
}
invatationListButton.snp.remakeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalTo(offStageButton.snp.trailing)
make.height.equalTo(32.scale375Height())
make.width.equalToSuperview().multipliedBy(1.0 / 3.0)
}
} else {
haveEnteredButton.snp.remakeConstraints { make in
make.left.centerY.equalToSuperview()
make.height.equalTo(32.scale375Height())
make.width.equalToSuperview().multipliedBy(0.5)
}
invatationListButton.snp.remakeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalTo(haveEnteredButton.snp.trailing)
make.height.equalTo(32.scale375Height())
make.width.equalToSuperview().multipliedBy(0.5)
}
}
}
func bindInteraction() {
viewModel.viewResponder = self
setupViewState()
searchBar.delegate = self
muteAllAudioButton.addTarget(self, action: #selector(muteAllAudioAction), for: .touchUpInside)
muteAllVideoButton.addTarget(self, action: #selector(muteAllVideoAction), for: .touchUpInside)
moreFunctionButton.addTarget(self, action: #selector(moreFunctionAction), for: .touchUpInside)
let tap = UITapGestureRecognizer(target: self, action: #selector(hideSearchControl(sender:)))
searchControl.addGestureRecognizer(tap)
haveEnteredButton.addTarget(self, action: #selector(selectAllUserAction(sender:)), for: .touchUpInside)
onStageButton.addTarget(self, action: #selector(selectOnStageAction(sender:)), for: .touchUpInside)
offStageButton.addTarget(self, action: #selector(selectOffStageAction(sender:)), for: .touchUpInside)
invatationListButton.addTarget(self, action: #selector(selectInvitationListAction(sender:)), for: .touchUpInside)
callEveryoneButton.addTarget(self, action: #selector(callEveryoneAction(sender:)), for: .touchUpInside)
invitationListPublisher
.receive(on: DispatchQueue.mainQueue)
.sink { [weak self] invitationList in
guard let self = self else { return }
let oldList = viewModel.invitationList
viewModel.invitationList = invitationList
if viewModel.userListType == .notInRoomUsers {
self.updateInvitationTableView(oldList: oldList, newList: invitationList)
self.updateBottomControlView()
}
self.updateListStateView()
}
.store(in: &cancellableSet)
}
func setupViewState() {
memberLabel.text = String(format: .memberText, viewModel.allUserCount)
let roomInfo = viewModel.roomInfo
muteAllAudioButton.isSelected = roomInfo.isMicrophoneDisableForAllUser
muteAllVideoButton.isSelected = roomInfo.isCameraDisableForAllUser
bottomControlView.isHidden = !viewModel.isShownBottomControlView
setupListStateViewText()
}
private func setupListStateViewText() {
if viewModel.isSeatEnabled {
let seatedListText: String = localizedReplace(.onStageNumberText, replace: String(viewModel.onStageCount))
onStageButton.setTitle(seatedListText, for: .normal)
let offSeatListText: String = localizedReplace(.notOnStageNumberText, replace: String(viewModel.offStageCount))
offStageButton.setTitle(offSeatListText, for: .normal)
} else {
let haveEnteredListText: String = localizedReplace(.haveEnterenRoomText, replace: String(viewModel.allUserCount))
haveEnteredButton.setTitle(haveEnteredListText, for: .normal)
}
let invitationListText: String = localizedReplace(.notEnteredRoomText, replace: String(viewModel.invitationUserList.count))
invatationListButton.setTitle(invitationListText, for: .normal)
}
private func updateBottomControlView() {
if viewModel.userListType == .notInRoomUsers {
bottomControlView.isHidden = true
callEveryoneButton.isHidden = viewModel.invitationList.isEmpty
} else {
callEveryoneButton.isHidden = true
bottomControlView.isHidden = !viewModel.isShownBottomControlView
}
setupUserListTableView()
}
@objc func muteAllAudioAction(sender: UIButton) {
viewModel.muteAllAudioAction(sender: sender, view: self)
}
@objc func muteAllVideoAction(sender: UIButton) {
viewModel.muteAllVideoAction(sender: sender, view: self)
}
@objc func moreFunctionAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .inviteViewType, height: 158.scale375Height())
}
@objc func hideSearchControl(sender: UIView) {
if #available(iOS 13, *) {
searchBar.searchTextField.resignFirstResponder()
} else {
searchBar.resignFirstResponder()
}
searchControl.isHidden = true
}
@objc func selectAllUserAction(sender: UIButton) {
guard sender.isSelected != true else { return }
sender.isSelected = true
invatationListButton.isSelected = false
viewModel.changeListState(type: .allUsers)
updateBottomControlView()
}
@objc func selectOnStageAction(sender: UIButton) {
guard sender.isSelected != true else { return }
sender.isSelected = true
offStageButton.isSelected = false
invatationListButton.isSelected = false
viewModel.changeListState(type: .onStageUsers)
updateBottomControlView()
}
@objc func selectOffStageAction(sender: UIButton) {
guard sender.isSelected != true else { return }
sender.isSelected = true
onStageButton.isSelected = false
invatationListButton.isSelected = false
viewModel.changeListState(type: .offStageUsers)
updateBottomControlView()
}
@objc func selectInvitationListAction(sender: UIButton) {
guard sender.isSelected != true else { return }
sender.isSelected = true
haveEnteredButton.isSelected = false
onStageButton.isSelected = false
offStageButton.isSelected = false
viewModel.changeListState(type: .notInRoomUsers)
updateBottomControlView()
}
@objc func callEveryoneAction(sender: UIButton) {
let userIdsNeedtoCall = viewModel.invitationList
.filter { $0.status != .pending }
.map { $0.invitee.userId }
conferenceStore.dispatch(action: ConferenceInvitationActions.inviteUsers(payload: (viewModel.roomInfo.roomId, userIdsNeedtoCall)))
}
func updateInvitationTableView(oldList: [TUIInvitation], newList: [TUIInvitation]) {
let result = viewModel.compareLists(oldList: oldList, newList: newList)
userListTableView.beginUpdates()
for invitation in result.removed {
if let index = oldList.firstIndex(where: { $0.invitee.userId == invitation.invitee.userId }) {
userListTableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
viewModel.attendeeList.remove(at: index)
}
}
for invitation in result.added {
viewModel.attendeeList.insert(UserEntity(invitation: invitation), at: 0)
userListTableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
}
for invitation in result.changed {
if let index = oldList.firstIndex(where: { $0.invitee.userId == invitation.invitee.userId }) {
if invitation.status == .rejected {
DispatchQueue.main.async {
if let cell = self.userListTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? UserListCell {
cell.showNotJoiningLabel()
}
}
}
userListTableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .none)
}
}
userListTableView.endUpdates()
}
deinit {
debugPrint("deinit \(self)")
}
@Injected(\.conferenceStore) private var conferenceStore
}
extension UserListView: UISearchBarDelegate {
func searchBar(_ searchBar:UISearchBar,textDidChange searchText:String){
let searchContentText = searchText.trimmingCharacters(in: .whitespaces)
viewModel.searchText = searchContentText
viewModel.isSearching = searchContentText.count != 0
viewModel.updateAttendeeList()
userListTableView.reloadData()
}
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
searchControl.isHidden = false
return true
}
}
extension UserListView: UITableViewDataSource {
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.attendeeList.count
}
}
extension UserListView: UITableViewDelegate {
internal func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let attendeeModel = viewModel.attendeeList[indexPath.row]
let cell = UserListCell(attendeeModel: attendeeModel, viewModel: viewModel)
cell.selectionStyle = .none
return cell
}
internal func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let attendeeModel = viewModel.attendeeList[indexPath.row]
viewModel.showUserManageViewAction(userId: attendeeModel.userId, userName: attendeeModel.userName)
}
internal func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60.scale375Height()
}
}
extension UserListView: UserListViewResponder {
func updateUserListTableView() {
setupUserListTableView()
}
func updateMemberLabel(count: Int) {
memberLabel.text = String(format: .memberText, viewModel.allUserCount)
}
func updateListStateView() {
setupListStateViewText()
}
func updateMuteAllAudioButtonState(isSelect: Bool) {
muteAllAudioButton.isSelected = isSelect
}
func updateMuteAllVideoButtonState(isSelect: Bool) {
muteAllVideoButton.isSelected = isSelect
}
func updateBottomControlView(isHidden: Bool) {
bottomControlView.isHidden = isHidden
}
func updateUserManagerViewDisplayStatus(isHidden: Bool) {
let model = UserListManagerViewModel(selectUserId: viewModel.userId)
let view = UserListManagerView(viewModel: model)
view.show(rootView: self)
}
func makeToast(text: String) {
RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
}
func reloadUserListView() {
userListTableView.reloadData()
}
func showAlert(title: String?, message: String?, sureTitle: String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) {
RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock)
}
}
private extension String {
static var allMuteAudioText: String {
localized("Mute All")
}
static var allMuteVideoText: String {
localized("Stop all video")
}
static var allUnMuteAudioText: String {
localized("Unmute all")
}
static var allUnMuteVideoText: String {
localized("Enable all video")
}
static var moreText: String {
localized("More")
}
static var memberText: String {
localized("Users(%lld)")
}
static var searchMemberText: String {
localized("Search for participants")
}
static var onStageNumberText: String {
localized("On stage(xx)")
}
static var notOnStageNumberText: String {
localized("Not on stage(xx)")
}
static var notEnteredRoomText: String {
localized("Not Entered(xx)")
}
static var haveEnterenRoomText: String {
localized("Entered(xx)")
}
static var callEveryoneText: String {
localized("Call all")
}
}