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,97 @@
//
// AlertTransitionAnimator.swift
// TUIRoomKit
//
// Created by janejntang on 2023/4/6.
//
import Foundation
class AlertTransitionAnimator : NSObject {
enum AlertTransitionStyle {
case present
case dismiss
}
enum AlertTransitionPosition {
case bottom
case right
}
var duration = 0.5
var alertTransitionStyle: AlertTransitionStyle = .present
var alertTransitionPosition: AlertTransitionPosition = .bottom
deinit {
debugPrint("deinit \(self)")
}
}
extension AlertTransitionAnimator {
private func presentTransition(transitionContext: UIViewControllerContextTransitioning) {
guard let fromController = transitionContext.viewController(forKey: .from), let toController = transitionContext.viewController(forKey: .to)
else { return }
guard let fromView = fromController.view, let toView = toController.view else { return }
let contentView = transitionContext.containerView
fromView.tintAdjustmentMode = .normal
fromView.isUserInteractionEnabled = false
toView.isUserInteractionEnabled = false
contentView.addSubview(toView)
switch alertTransitionPosition {
case .bottom:
toView.frame = CGRect(x: 0, y: contentView.bounds.size.height, width: contentView.bounds.size.width, height:
contentView.bounds.size.height)
case .right:
toView.frame = CGRect(x: contentView.bounds.size.width, y: 0, width: contentView.bounds.size.width/2, height:
contentView.bounds.size.height)
}
UIView.animate(withDuration: duration, animations: { [weak self] in
guard let self = self else { return }
switch self.alertTransitionPosition {
case .bottom:
toView.frame = CGRect(x: 0, y: 0, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
case .right:
toView.frame = CGRect(x: 0, y: 0, width: contentView.bounds.size.width, height: contentView.bounds.size.height)
}
}) { (finish) in
fromView.isUserInteractionEnabled = true
toView.isUserInteractionEnabled = true
transitionContext.completeTransition(true)
}
}
private func dismissTransition(transitionContext: UIViewControllerContextTransitioning) {
guard let fromController = transitionContext.viewController(forKey: .from), let toController = transitionContext.viewController(forKey: .to)
else { return }
guard let fromView = fromController.view, let toView = toController.view else { return }
fromView.isUserInteractionEnabled = false
toView.isUserInteractionEnabled = false
let contentView = transitionContext.containerView
UIView.animate(withDuration: duration, animations: { [weak self] in
guard let self = self else { return }
switch self.alertTransitionPosition {
case .bottom:
fromView.frame = CGRect(x: 0, y: contentView.bounds.size.height, width: contentView.bounds.size.width, height:
contentView.bounds.size.height)
case .right:
fromView.frame = CGRect(x: contentView.bounds.size.width, y: 0, width: contentView.bounds.size.width, height:
contentView.bounds.size.height)
}
}) { (finish) in
fromView.removeFromSuperview()
toView.isUserInteractionEnabled = true
transitionContext.completeTransition(true)
}
}
}
extension AlertTransitionAnimator: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
switch alertTransitionStyle {
case .present:
presentTransition(transitionContext: transitionContext)
case .dismiss:
dismissTransition(transitionContext: transitionContext)
}
}
}

View File

@@ -0,0 +1,64 @@
//
// BroadcastLauncher.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/17.
// Copyright © 2023 Tencent. All rights reserved.
//
import ReplayKit
import UIKit
@available(iOS 12.0, *)
class BroadcastLauncher: NSObject {
var systemExtensionPicker = RPSystemBroadcastPickerView()
var prevLaunchEventTime: CFTimeInterval = 0
static let sharedInstance = BroadcastLauncher()
override private init() {
super.init()
let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
picker.showsMicrophoneButton = false
picker.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin]
systemExtensionPicker = picker
if let pluginPath = Bundle.main.builtInPlugInsPath,
let contents = try? FileManager.default.contentsOfDirectory(atPath: pluginPath) {
for content in contents where content.hasSuffix(".appex") {
guard let bundle = Bundle(path: URL(fileURLWithPath: pluginPath).appendingPathComponent(content).path),
let identifier: String = (bundle.infoDictionary?["NSExtension"] as? [String: Any])? ["NSExtensionPointIdentifier"] as? String
else {
continue
}
if identifier == "com.apple.broadcast-services-upload" {
picker.preferredExtension = bundle.bundleIdentifier
break
}
}
}
}
static func launch() {
BroadcastLauncher.sharedInstance.launch()
}
func launch() {
let now = CFAbsoluteTimeGetCurrent()
if now - prevLaunchEventTime < 1.0 {
return
}
prevLaunchEventTime = now
for view in systemExtensionPicker.subviews {
if let button = view as? UIButton {
button.sendActions(for: .allTouchEvents)
break
}
}
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,123 @@
//
// ButtonItemView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/10.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
class ButtonItemView: UIView {
var itemData: ButtonItemData
lazy var controlButton: UIButton = {
let button = UIButton()
button.backgroundColor = itemData.backgroundColor
if let cornerRadius = itemData.cornerRadius {
button.layer.cornerRadius = cornerRadius
}
return button
}()
lazy var label: UILabel = {
let label = UILabel()
label.textAlignment = isRTL ? .right : .left
label.font = itemData.titleFont ?? UIFont(name: "PingFangSC-Regular", size: 14)
label.textColor = itemData.titleColor ?? UIColor(0xD5E0F2)
label.adjustsFontSizeToFitWidth = true
return label
}()
lazy var imageView: UIImageView = {
let view = UIImageView()
return view
}()
lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x4F586B,alpha: 0.1)
view.isHidden = itemData.hasLineView ? false : true
return view
}()
// MARK: - initialized function
init(itemData: ButtonItemData) {
self.itemData = itemData
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(lineView)
addSubview(controlButton)
controlButton.addSubview(imageView)
controlButton.addSubview(label)
}
func activateConstraints() {
controlButton.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
imageView.snp.makeConstraints { make in
if let size = itemData.imageSize {
make.size.equalTo(size)
} else {
make.width.height.equalTo(20)
}
if itemData.orientation == .left {
make.leading.equalToSuperview()
} else {
make.trailing.equalToSuperview()
}
make.centerY.equalToSuperview()
}
label.snp.makeConstraints { make in
make.centerY.equalToSuperview()
if itemData.orientation == .left {
make.leading.equalTo(imageView.snp.trailing).offset(10)
} else {
make.trailing.equalTo(imageView.snp.leading).offset(-10)
}
}
lineView.snp.makeConstraints { make in
make.bottom.equalToSuperview()
make.leading.trailing.equalToSuperview()
make.height.equalTo(1.scale375())
}
}
func bindInteraction() {
setupViewState(item: itemData)
controlButton.addTarget(self, action: #selector(clickMenuButton(sender:)), for: .touchUpInside)
}
func setupViewState(item: ButtonItemData) {
itemData = item
controlButton.isSelected = item.isSelect
controlButton.isEnabled = item.isEnabled
imageView.image = item.isSelect ? itemData.selectedImage : itemData.normalImage
label.text = item.isSelect ? itemData.selectedTitle : itemData.normalTitle
}
@objc func clickMenuButton(sender: UIButton) {
itemData.action?(sender)
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,212 @@
//
// ListCellItemView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/6.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
class ListCellItemView: UIView {
let itemData: ListCellItemData
let titleLabel: UILabel = {
let view = UILabel()
view.backgroundColor = .clear
view.textColor = UIColor(0x8F9AB2)
view.font = UIFont(name: "PingFangSC-Medium", size: 14)
view.minimumScaleFactor = 0.5
return view
}()
let messageLabel: UILabel = {
let view = UILabel()
view.backgroundColor = .clear
view.textColor = UIColor(0xD5E0F2)
view.font = UIFont(name: "PingFangSC-Medium", size: 14)
view.adjustsFontSizeToFitWidth = false
view.minimumScaleFactor = 0.5
return view
}()
let slider: UISlider = {
let view = UISlider()
return view
}()
let sliderLabel: UILabel = {
let view = UILabel()
view.textAlignment = isRTL ? .left : .right
view.backgroundColor = .clear
view.textColor = UIColor(0xD1D9EC)
view.font = UIFont(name: "PingFangSC-Medium", size: 14)
view.adjustsFontSizeToFitWidth = true
view.textAlignment = .center
return view
}()
let rightSwitch: UISwitch = {
let view = UISwitch()
view.isOn = true
view.onTintColor = UIColor(0x0062E3)
return view
}()
lazy var rightButton: ButtonItemView = {
let button = ButtonItemView(itemData: itemData.buttonData ?? ButtonItemData())
return button
}()
let downLineView : UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x6B758A,alpha: 0.3)
return view
}()
init(itemData: ListCellItemData) {
self.itemData = itemData
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 }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(titleLabel)
addSubview(messageLabel)
addSubview(slider)
addSubview(sliderLabel)
addSubview(rightSwitch)
addSubview(rightButton)
addSubview(downLineView)
}
func activateConstraints() {
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.centerY.equalToSuperview()
make.width.equalTo(100.scale375())
make.height.equalTo(20.scale375())
}
messageLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel.snp.trailing).offset(16.scale375())
make.centerY.equalToSuperview()
if itemData.hasRightButton {
make.trailing.equalTo(rightButton.snp.leading)
} else if (itemData.hasSwitch) {
make.trailing.equalTo(rightSwitch.snp.leading)
} else {
make.trailing.equalToSuperview()
}
make.height.equalTo(20.scale375())
}
slider.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.width.equalTo(152.scale375())
make.centerY.equalToSuperview()
}
sliderLabel.snp.makeConstraints { make in
make.leading.equalTo(titleLabel.snp.trailing).offset(5.scale375())
make.trailing.equalTo(slider.snp.leading).offset(-5.scale375())
make.centerY.equalToSuperview()
}
rightSwitch.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalToSuperview()
}
rightButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalToSuperview()
if let size = itemData.buttonData?.size {
make.width.equalTo(size.width)
make.height.equalTo(size.height)
} else {
make.width.equalTo(57.scale375())
make.height.equalTo(20.scale375Height())
}
}
downLineView.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.bottom.equalToSuperview()
make.height.equalTo(0.5)
}
}
func bindInteraction() {
setupViewState(item: itemData)
if itemData.hasOverAllAction {
let tap = UITapGestureRecognizer(target: self, action: #selector(overAllAction(sender:)))
addGestureRecognizer(tap)
}
rightSwitch.addTarget(self, action: #selector(switchAction(sender:)), for: .touchUpInside)
slider.addTarget(self, action: #selector(sliderAction(sender:)), for: .valueChanged)
}
func setupViewState(item: ListCellItemData) {
titleLabel.isHidden = item.titleText.isEmpty
titleLabel.text = item.titleText
if let titleColor = item.titleColor {
titleLabel.textColor = titleColor
}
messageLabel.isHidden = item.messageText.isEmpty
messageLabel.text = item.messageText
if let messageColor = item.messageColor {
messageLabel.textColor = messageColor
}
rightSwitch.isHidden = !item.hasSwitch
rightSwitch.isOn = item.isSwitchOn
rightButton.isHidden = !item.hasRightButton
if let buttonData = item.buttonData {
rightButton.setupViewState(item: buttonData)
}
slider.isHidden = !item.hasSlider
sliderLabel.isHidden = !item.hasSliderLabel
slider.minimumValue = item.minimumValue / item.sliderStep
slider.maximumValue = item.maximumValue / item.sliderStep
slider.value = item.sliderDefault / item.sliderStep
sliderLabel.text = String(Int(slider.value) * Int(item.sliderStep)) + item.sliderUnit
downLineView.isHidden = !itemData.hasDownLineView
}
@objc func overAllAction(sender: UIView) {
itemData.action?(sender)
}
@objc func rightButtonAction(sender: UIButton) {
itemData.action?(sender)
}
@objc func switchAction(sender: UISwitch) {
itemData.action?(sender)
}
@objc func textFieldAction(sender: UITextField) {
itemData.action?(sender)
}
@objc func sliderAction(sender: UISlider) {
sliderLabel.text = String(Int(slider.value) * Int(itemData.sliderStep)) + itemData.sliderUnit
itemData.action?(sender)
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,227 @@
//
// PrepareSettingItemView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/6.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import UIKit
class PrepareSettingItemView: UIView {
let itemData: PrepareSettingItemData
let titleLabel: UILabel = {
let view = UILabel()
view.backgroundColor = .clear
view.textColor = UIColor(0xD1D9EC)
view.font = UIFont.systemFont(ofSize: 14, weight: .regular)
view.minimumScaleFactor = 0.5
return view
}()
let messageLabel: UILabel = {
let view = UILabel()
view.backgroundColor = .clear
view.textColor = UIColor(0xD1D9EC)
view.font = UIFont.systemFont(ofSize: 14, weight: .medium)
view.adjustsFontSizeToFitWidth = false
view.numberOfLines = 0
view.minimumScaleFactor = 0.5
return view
}()
let textField: UITextField = {
let view = UITextField()
view.backgroundColor = .clear
view.textColor = UIColor(0xD1D9EC)
view.font = UIFont(name: "PingFangSC-Regular", size: 14)
let color = UIColor(0xBBBBBB)
view.keyboardType = .numberPad
return view
}()
let rightSwitch: UISwitch = {
let view = UISwitch()
view.isOn = true
view.onTintColor = UIColor(0x0062E3)
return view
}()
lazy var rightButton: UIButton = {
let button = UIButton(type: .custom)
let normalIcon = UIImage(named: "room_drop_down")
button.setImage(normalIcon, for: .normal)
return button
}()
let downLineView : UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x6B758A,alpha: 0.3)
return view
}()
init(itemData: PrepareSettingItemData) {
self.itemData = itemData
super.init(frame: .zero)
self.setupViewState(item: itemData)
}
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 }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
private func constructViewHierarchy() {
addSubview(titleLabel)
addSubview(messageLabel)
addSubview(textField)
addSubview(rightSwitch)
addSubview(rightButton)
addSubview(downLineView)
}
private func activateConstraints() {
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20)
make.centerY.equalToSuperview()
make.width.equalTo(100.scale375())
}
messageLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel.snp.right).offset(10.scale375())
make.trailing.equalToSuperview().offset(-40)
make.centerY.equalToSuperview()
}
textField.snp.makeConstraints { make in
make.left.equalTo(titleLabel.snp.right).offset(10)
make.trailing.equalToSuperview().offset(-100)
make.centerY.equalToSuperview()
}
rightSwitch.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20)
make.centerY.equalToSuperview()
make.width.equalTo(42)
make.height.equalTo(24)
}
rightButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().offset(-20)
}
downLineView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20)
make.trailing.equalToSuperview().offset(-20)
make.bottom.equalToSuperview()
make.height.equalTo(1)
}
}
private func bindInteraction() {
if itemData.hasOverAllAction {
let tap = UITapGestureRecognizer(target: self, action: #selector(overAllAction(sender:)))
addGestureRecognizer(tap)
}
rightButton.addTarget(self, action: #selector(rightButtonAction(sender:)), for: .touchUpInside)
rightSwitch.addTarget(self, action: #selector(switchAction(sender:)), for: .touchUpInside)
}
func setupViewState(item: PrepareSettingItemData) {
if item.titleText.isEmpty {
titleLabel.isHidden = true
}
if item.messageText.isEmpty {
messageLabel.isHidden = true
}
if !item.hasFieldView {
textField.isHidden = true
}
if !item.hasSwitch {
rightSwitch.isHidden = true
}
if !item.hasButton {
rightButton.isHidden = true
}
rightSwitch.isOn = item.isSwitchOn
titleLabel.text = item.titleText
messageLabel.text = item.messageText
textField.isUserInteractionEnabled = item.fieldEnable
textField.delegate = self
if item.fieldEnable {
let color = UIColor(0xBBBBBB)
textField.attributedPlaceholder = NSAttributedString(string: item.fieldPlaceholderText,attributes:
[NSAttributedString.Key.foregroundColor:color])
} else {
textField.text = item.fieldText
}
downLineView.isHidden = !item.hasDownLineView
}
@objc func overAllAction(sender: UIView) {
itemData.action?(sender)
}
@objc func rightButtonAction(sender: UIButton) {
itemData.action?(sender)
}
@objc func switchAction(sender: UISwitch) {
itemData.action?(sender)
}
@objc func textFieldAction(sender: UITextField) {
itemData.action?(sender)
}
deinit {
debugPrint("deinit \(self)")
}
}
extension PrepareSettingItemView: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let maxCount = 11
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.05) {
textField.text = textField.text?
.replacingOccurrences(of: " ",
with: "",
options: .literal,
range: nil)
.addIntervalSpace(intervalStr: " ", interval: 3)
}
guard let textFieldText = textField.text,
let rangeOfTextToReplace = Range(range, in: textFieldText) else {
return false
}
let substringToReplace = textFieldText[rangeOfTextToReplace]
if substringToReplace.count > 0 && string.count == 0 {
return true
}
let count = textFieldText.count - substringToReplace.count + string.count
let res = count <= maxCount
return res
}
func textFieldDidChangeSelection(_ textField: UITextField) {
textFieldAction(sender: textField)
}
func textFieldDidEndEditing(_ textField: UITextField) {
textFieldAction(sender: textField)
}
}

View File

@@ -0,0 +1,249 @@
//
// QRCodeView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/11.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
class QRCodeView: UIView {
let viewModel: QRCodeViewModel
let backButton: UIButton = {
let button = UIButton()
button.contentVerticalAlignment = .center
button.contentHorizontalAlignment = isRTL ? .right : .left
button.setTitleColor(UIColor(0xADB6CC), for: .normal)
let image = UIImage(named: "room_back_white", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn()
button.setImage(image, for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 18)
button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 25, bottom: 0, right: 0)
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 0)
return button
}()
let middleView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x2A2D38)
return view
}()
let titleLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xD1D9EC)
label.font = UIFont(name: "PingFangSC-Regular", size: 28)
label.textAlignment = .center
label.backgroundColor = .clear
label.adjustsFontSizeToFitWidth = true
return label
}()
let roomIdView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
let roomIdLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xD1D9EC)
label.font = UIFont(name: "PingFangSC-Regular", size: 20)
label.adjustsFontSizeToFitWidth = true
label.textAlignment = .center
return label
}()
let copyButton: UIButton = {
let button = UIButton()
let normalIcon = UIImage(named: "room_copy", in: tuiRoomKitBundle(), compatibleWith: nil)
button.setImage(normalIcon, for: .normal)
return button
}()
let qrCodeView: UIView = {
let view = UIView()
view.backgroundColor = .white
return view
}()
let qrCodeImageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
let qrCodeLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.text = .scanCodeText
label.textAlignment = .center
return label
}()
let tencentImageView: UIImageView = {
let image = UIImage(named: "room_tencent", in: tuiRoomKitBundle(), compatibleWith: nil)
let imageView = UIImageView(image: image)
return imageView
}()
let bottomButton: UIButton = {
let button = UIButton()
button.backgroundColor = UIColor(0x006CFF)
button.setTitle(.saveIntoAlbumText, for: .normal)
button.backgroundColor = UIColor(0x006CFF)
button.setTitleColor(.white, for: .normal)
return button
}()
init(viewModel: QRCodeViewModel) {
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 }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
override func draw(_ rect: CGRect) {
super.draw(rect)
middleView.roundedRect(rect: middleView.bounds,
byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight],
cornerRadii: CGSize(width: 12, height: 12))
qrCodeView.roundedRect(rect: qrCodeView.bounds,
byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight],
cornerRadii: CGSize(width: 12, height: 12))
bottomButton.roundedRect(rect: bottomButton.bounds,
byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight],
cornerRadii: CGSize(width: 12, height: 12))
}
func constructViewHierarchy() {
addSubview(backButton)
addSubview(middleView)
addSubview(bottomButton)
middleView.addSubview(titleLabel)
middleView.addSubview(roomIdView)
middleView.addSubview(qrCodeView)
middleView.addSubview(tencentImageView)
roomIdView.addSubview(roomIdLabel)
roomIdView.addSubview(copyButton)
qrCodeView.addSubview(qrCodeImageView)
qrCodeView.addSubview(qrCodeLabel)
}
func activateConstraints() {
backButton.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.top.equalTo(safeAreaLayoutGuide.snp.top)
make.height.equalTo(20)
make.width.equalTo(200)
}
middleView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12.scale375())
make.trailing.equalToSuperview().offset(-12.scale375())
make.height.equalTo(399.scale375())
make.centerY.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(24.scale375())
make.height.equalTo(30.scale375())
make.width.equalToSuperview()
}
roomIdView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(4.scale375())
make.width.equalTo(120.scale375())
make.centerX.equalToSuperview()
make.height.equalTo(22.scale375())
}
roomIdLabel.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.height.equalToSuperview()
make.width.equalTo(80.scale375())
}
copyButton.snp.makeConstraints { make in
make.leading.equalTo(roomIdLabel.snp.trailing).offset(3)
make.centerY.equalToSuperview()
make.width.height.equalTo(22.scale375())
}
qrCodeView.snp.makeConstraints { make in
make.top.equalTo(roomIdView.snp.bottom).offset(10)
make.centerX.equalToSuperview()
make.width.equalTo(210.scale375())
make.height.equalTo(242.scale375())
}
qrCodeImageView.snp.makeConstraints { make in
make.width.height.equalTo(162.scale375())
make.top.equalToSuperview().offset(24.scale375())
make.centerX.equalToSuperview()
}
qrCodeLabel.snp.makeConstraints { make in
make.width.equalToSuperview()
make.height.equalTo(20.scale375())
make.top.equalTo(qrCodeImageView.snp.bottom).offset(24.scale375())
}
tencentImageView.snp.makeConstraints { make in
make.top.equalTo(qrCodeView.snp.bottom).offset(20.scale375())
make.centerX.equalToSuperview()
make.width.equalTo(86.scale375())
make.height.equalTo(23.scale375())
}
bottomButton.snp.makeConstraints { make in
make.width.equalTo(200.scale375())
make.height.equalTo(52.scale375())
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-30.scale375())
}
}
func bindInteraction() {
setupViewState()
backButton.addTarget(self, action: #selector(backAction(sender:)), for: .touchUpInside)
copyButton.addTarget(self, action: #selector(copyAction(sender:)), for: .touchUpInside)
bottomButton.addTarget(self, action: #selector(saveIntoAlbumAction(sender:)), for: .touchUpInside)
}
func setupViewState() {
backgroundColor = UIColor(0x17181F)
titleLabel.text = viewModel.store.roomInfo.name
roomIdLabel.text = viewModel.store.roomInfo.roomId
viewModel.createQRCodeImageView(url: viewModel.urlString, imageView: qrCodeImageView)
}
@objc func backAction(sender: UIButton) {
viewModel.backAction()
}
@objc func copyAction(sender: UIButton) {
viewModel.copyAction(sender: sender, text: roomIdLabel.text ?? "")
}
@objc func saveIntoAlbumAction(sender: UIButton) {
guard let image = qrCodeImageView.image else { return }
viewModel.saveIntoAlbumAction(sender: sender, image: image)
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var scanCodeText: String {
localized("Scan the code to enter the conference")
}
static var saveIntoAlbumText: String {
localized("Save into the album")
}
}

View File

@@ -0,0 +1,33 @@
//
// RoomKitNavigationController.swift
// TUIRoomKit
//
// Created by janejntang on 2023/4/20.
//
import Foundation
class RoomKitNavigationController: UINavigationController {
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
interactivePopGestureRecognizer?.isEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
guard let supportedInterfaceOrientations = topViewController?.supportedInterfaceOrientations as? UIInterfaceOrientationMask
else { return .portrait }
return supportedInterfaceOrientations
}
override var shouldAutorotate: Bool {
guard let shouldAutorotate = topViewController?.shouldAutorotate else { return false }
return shouldAutorotate
}
}

View File

@@ -0,0 +1,286 @@
//
// ConferenceInvitationViewController.swift
// TUIRoomKit
//
// Created by jeremiawang on 2024/8/6.
//
import Foundation
import RTCRoomEngine
import UIKit
import Combine
import Factory
class ConferenceInvitationViewController: UIViewController {
private var cancellableSet = Set<AnyCancellable>()
var roomInfo: TUIRoomInfo
var invitation: TUIInvitation
init(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
self.roomInfo = roomInfo
self.invitation = invitation
super.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .fullScreen
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public var shouldAutorotate: Bool {
return false
}
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
private let backgroundImageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.alpha = 0.2
return view
}()
private let avatarImageView: UIImageView = {
let avatarImageView = UIImageView()
avatarImageView.contentMode = .scaleAspectFill
avatarImageView.layer.cornerRadius = 25
avatarImageView.clipsToBounds = true
return avatarImageView
}()
private let inviteLabel: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
return label
}()
private let conferenceNameLabel: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = UIFont.boldSystemFont(ofSize: 24)
label.numberOfLines = 1
label.lineBreakMode = .byTruncatingTail
return label
}()
private let detailsLabel: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
return label
}()
private let joinSliderView: UIView = {
let view = UIView()
view.backgroundColor = .white.withAlphaComponent(0.1)
view.layer.cornerRadius = 39.scale375()
return view
}()
private let joinLabel: UILabel = {
let label = UILabel()
label.text = .joinNowText
label.textColor = .white.withAlphaComponent(0.8)
label.font = UIFont(name: "PingFangSC-Medium", size: 16)
label.textAlignment = .center
return label
}()
private let sliderThumbView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.tui_color(withHex: "1C66E5")
view.layer.cornerRadius = 32
return view
}()
private let arrowImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "room_rightlink_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
imageView.tintColor = .white
return imageView
}()
private let declineButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle(.notEnterText, for: .normal)
button.setTitleColor(.white.withAlphaComponent(0.8), for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Medium", size: 16)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
initState()
initializeData()
constructViewHierarchy()
activateConstraints()
bindInteraction()
operation.select(ViewSelectors.getDismissInvitationFlag)
.receive(on: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] shouldDismissInvitation in
guard let self = self else { return }
if shouldDismissInvitation {
InvitationObserverService.shared.dismissInvitationWindow()
}
}
.store(in: &cancellableSet)
}
deinit {
debugPrint("deinit \(self)")
}
private func initState() {
self.operation.dispatch(action: InvitationViewActions.resetInvitationFlag())
}
private func constructViewHierarchy() {
view.addSubview(backgroundImageView)
view.addSubview(avatarImageView)
view.addSubview(inviteLabel)
view.addSubview(conferenceNameLabel)
view.addSubview(detailsLabel)
view.addSubview(joinSliderView)
joinSliderView.addSubview(joinLabel)
joinSliderView.addSubview(sliderThumbView)
sliderThumbView.addSubview(arrowImageView)
view.addSubview(declineButton)
}
private func activateConstraints() {
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
avatarImageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.height.width.equalTo(50.scale375())
make.top.equalToSuperview().offset(150.scale375Height())
}
inviteLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(avatarImageView.snp.bottom).offset(16)
}
conferenceNameLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.width.lessThanOrEqualTo(300.scale375())
make.top.equalTo(inviteLabel.snp.bottom).offset(30)
}
detailsLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(conferenceNameLabel.snp.bottom).offset(10)
}
declineButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-94.scale375Height())
}
joinSliderView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.width.equalTo(200.scale375())
make.height.equalTo(78.scale375())
make.bottom.equalTo(declineButton.snp.top).offset(-30)
}
joinLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.centerX.equalToSuperview().offset(32.scale375())
}
sliderThumbView.snp.makeConstraints { make in
make.left.equalTo(joinSliderView.snp.left).offset(5)
make.centerY.equalTo(joinSliderView.snp.centerY)
make.width.height.equalTo(64.scale375())
}
arrowImageView.snp.makeConstraints { make in
make.center.equalTo(sliderThumbView)
make.width.height.equalTo(20.scale375())
}
}
private func bindInteraction() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
sliderThumbView.addGestureRecognizer(panGesture)
declineButton.addTarget(self, action: #selector(rejectAction), for: .touchUpInside)
}
private func initializeData() {
inviteLabel.text = invitation.inviter.userName + .inviteJoinConferenceText
conferenceNameLabel.text = roomInfo.name
detailsLabel.text = .hostText + roomInfo.ownerName + " | " + .participantText + String(roomInfo.memberCount)
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
if let url = URL(string: invitation.inviter.avatarUrl) {
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
backgroundImageView.sd_setImage(with: url, placeholderImage: placeholder)
} else {
avatarImageView.image = placeholder
backgroundImageView.image = placeholder
}
}
@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: joinSliderView)
let maxTranslation = joinSliderView.frame.width - sliderThumbView.frame.width - 10
switch gesture.state {
case .changed:
if translation.x >= 0 && translation.x <= maxTranslation {
sliderThumbView.snp.updateConstraints { make in
make.left.equalTo(joinSliderView.snp.left).offset(5 + translation.x)
}
}
case .ended:
if translation.x >= maxTranslation {
sliderThumbView.snp.updateConstraints { make in
make.left.equalTo(joinSliderView.snp.left).offset(5 + maxTranslation)
}
gesture.isEnabled = false
self.acceptAction()
} else {
sliderThumbView.snp.updateConstraints { make in
make.left.equalTo(joinSliderView.snp.left).offset(5)
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
default:
break
}
}
private func acceptAction() {
operation.dispatch(action: ConferenceInvitationActions.accept(payload: roomInfo.roomId))
}
@objc func rejectAction() {
operation.dispatch(action: InvitationViewActions.dismissInvitationView())
operation.dispatch(action: ConferenceInvitationActions.reject(payload: (roomInfo.roomId, .rejectToEnter)))
}
@Injected(\.navigation) var route
@Injected(\.conferenceStore) var operation
}
private extension String {
static var inviteJoinConferenceText: String {
localized(" invite you to join the conference")
}
static var hostText: String {
localized("Conference Host")
}
static var participantText: String {
localized("Participant")
}
static var peopleText: String {
localized("People")
}
static var joinNowText: String {
localized("Join now")
}
static var notEnterText: String {
localized("Do not enter for now")
}
}

View File

@@ -0,0 +1,194 @@
//
// ConferenceOptionCell.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/6/6.
//
import Foundation
import UIKit
import Factory
class ConferenceListCell: UITableViewCell {
@Injected(\.conferenceStore) private var store
@Injected(\.conferenceMainViewStore) private var viewStore
static let reusedIdentifier = "ConferenceListCell"
private var conferenceInfo: ConferenceInfo?
let roomNameLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont(name: "PingFangSC-Medium", size: 16)
label.textColor = UIColor.tui_color(withHex: "4F586B")
return label
}()
let interactiveIcon: UIImageView = {
let image = UIImage(named: "room_right_black_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
let imageView = UIImageView(image: image)
return imageView
}()
let detailLabel: UILabel = {
let label = UILabel(frame: .zero)
return label
}()
let enterButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle(.enterText, for: .normal)
button.setTitleColor(UIColor.tui_color(withHex: "#4E5461"), for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Medium", size: 14)
button.backgroundColor = UIColor.tui_color(withHex: "F0F3FA")
button.sizeToFit()
button.layer.cornerRadius = button.frame.height / 2
return button
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
private func constructViewHierarchy() {
contentView.addSubview(roomNameLabel)
contentView.addSubview(interactiveIcon)
contentView.addSubview(detailLabel)
contentView.addSubview(enterButton)
}
private func activateConstraints() {
enterButton.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.width.greaterThanOrEqualTo(68)
make.top.equalToSuperview().offset(8)
}
interactiveIcon.snp.makeConstraints { make in
make.trailing.lessThanOrEqualTo(enterButton.snp.leading).offset(-5)
make.width.height.equalTo(16)
make.centerY.equalTo(roomNameLabel)
}
roomNameLabel.snp.makeConstraints { make in
make.trailing.equalTo(interactiveIcon.snp.leading)
make.leading.equalToSuperview()
make.top.equalToSuperview()
}
detailLabel.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.top.equalTo(roomNameLabel.snp.bottom).offset(6)
make.trailing.lessThanOrEqualTo(enterButton.snp.leading).offset(-20)
}
}
private func bindInteraction() {
enterButton.addTarget(self, action: #selector(enterAction(sender:)), for: .touchUpInside)
}
@objc func enterAction(sender: UIButton) {
guard let info = conferenceInfo else {
return
}
if !info.basicInfo.roomId.isEmpty {
store.dispatch(action: RoomActions.joinConference(payload: info.basicInfo.roomId))
store.dispatch(action: ScheduleViewActions.popDetailView())
viewStore.updateInternalCreation(isInternalCreation: true)
}
}
func updateCell(with info: ConferenceInfo) {
conferenceInfo = info
roomNameLabel.text = info.basicInfo.name
detailLabel.attributedText = getAttributedText(from: info)
}
private func getAttributedText(from info: ConferenceInfo) -> NSMutableAttributedString {
let normalAttributes: [NSAttributedString.Key: Any] =
[.font: UIFont.systemFont(ofSize: 14), .foregroundColor: UIColor.tui_color(withHex: "4F586B")]
let duration = getDuration(from: info)
var result = NSMutableAttributedString(string: duration, attributes: normalAttributes)
addDelimiter(to: &result)
let roomId = addSpaces(to: info.basicInfo.roomId)
let roomIdAtrributeString = NSMutableAttributedString(string: roomId, attributes: normalAttributes)
result.append(roomIdAtrributeString)
guard info.status == .running else { return result }
addDelimiter(to: &result)
let status = getStatusString(from: info)
let statusAttributes: [NSAttributedString.Key: Any] =
[.font:UIFont.systemFont(ofSize: 14), .foregroundColor: UIColor.tui_color(withHex: "1C66E5")]
let statusAtrributeString = NSMutableAttributedString(string: status, attributes: statusAttributes)
result.append(statusAtrributeString)
return result
}
private func addDelimiter(to attributeString: inout NSMutableAttributedString) {
let delimiterAtrributeString = NSMutableAttributedString(string:" | ",
attributes: [
.font: UIFont.systemFont(ofSize: 11),
.foregroundColor: UIColor.tui_color(withHex: "969EB4"),
.baselineOffset: 2
])
attributeString.append(delimiterAtrributeString)
}
private func addSpaces(to string: String) -> String {
var result = ""
for (index, char) in string.enumerated() {
if index > 0 && index % 3 == 0 {
result += " "
}
result += String(char)
}
return result
}
private func getDuration(from info: ConferenceInfo) -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = .current
dateFormatter.dateFormat = "HH:mm"
let startDate = Date(timeIntervalSince1970: TimeInterval(info.scheduleStartTime))
let endDate = Date(timeIntervalSince1970: TimeInterval(info.scheduleEndTime))
let startString = dateFormatter.string(from: startDate)
let endString = dateFormatter.string(from: endDate)
return startString + " - " + endString
}
private func getStatusString(from info: ConferenceInfo) -> String {
if info.status == .running {
return .inProgressText
}
return ""
}
}
private extension String {
static var enterText: String {
localized("Enter")
}
static var inProgressText: String {
localized("Ongoing")
}
}

View File

@@ -0,0 +1,322 @@
//
// SelectedMembersViewController.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/6/24.
//
import Foundation
import RTCRoomEngine
protocol SelectedMemberCellProtocol: AnyObject {
func didDeleteButtonClicked(in memberCell: SelectedMemberCell)
}
class SelectedMembersViewController: UIViewController {
private(set) var showDeleteButton: Bool = true
var selectedMember: [UserInfo] = []
var didDeselectMember: ((UserInfo) -> Void)?
private let arrowViewHeight: CGFloat = 35.0
init(showDeleteButton: Bool = true, selectedMembers: [UserInfo] = []) {
self.showDeleteButton = showDeleteButton
self.selectedMember = selectedMembers
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .clear
tableView.register(SelectedMemberCell.self, forCellReuseIdentifier: SelectedMemberCell.reuseIdentifier)
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = 0
}
return tableView
}()
private let dropArrowView : UIView = {
let view = UIView()
view.backgroundColor = .white
return view
}()
private let dropArrowImageView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "room_drop_arrow", in:tuiRoomKitBundle(), compatibleWith: nil)
return view
}()
let contentView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .white
view.layer.cornerRadius = 8
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.tui_color(withHex: "0F1014", alpha: 0.6)
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
private func constructViewHierarchy() {
view.addSubview(contentView)
dropArrowView.addSubview(dropArrowImageView)
contentView.addSubview(dropArrowView)
contentView.addSubview(tableView)
}
private func activateConstraints() {
contentView.snp.makeConstraints { make in
make.height.equalTo(610)
make.leading.bottom.trailing.equalToSuperview()
}
dropArrowView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(arrowViewHeight)
}
dropArrowImageView.snp.makeConstraints { make in
make.centerX.centerY.equalToSuperview()
make.width.equalTo(24.scale375())
make.height.equalTo(3.scale375())
}
tableView.snp.makeConstraints { make in
make.top.equalTo(dropArrowView.snp.bottom)
make.leading.bottom.trailing.equalToSuperview()
}
}
private func bindInteraction() {
tableView.delegate = self
tableView.dataSource = self
let dropArrowTap = UITapGestureRecognizer(target: self, action: #selector(dropDownPopUpViewAction(sender:)))
dropArrowView.addGestureRecognizer(dropArrowTap)
dropArrowView.isUserInteractionEnabled = true
}
@objc func dropDownPopUpViewAction(sender: UIView) {
self.dismiss(animated: true)
}
}
extension SelectedMembersViewController: SelectedMemberCellProtocol{
func didDeleteButtonClicked(in memberCell: SelectedMemberCell) {
guard let indexPath = self.tableView.indexPath(for: memberCell) else {
return
}
let member = selectedMember[indexPath.row]
self.didDeselectMember?(member)
selectedMember.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .none)
guard let headerView = tableView.headerView(forSection: 0) as? SelectedMemberHeaderView else {
return
}
headerView.updateLabel(with: selectedMember.count)
}
}
extension SelectedMembersViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 52
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = SelectedMemberHeaderView(reuseIdentifier: "CustomHeaderView")
headerView.updateLabel(with: selectedMember.count)
return headerView
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 38
}
}
extension SelectedMembersViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return selectedMember.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: SelectedMemberCell.reuseIdentifier, for: indexPath)
as? SelectedMemberCell else {
return UITableViewCell()
}
let member = selectedMember[indexPath.row]
cell.updateView(with: member)
cell.delegate = self
if showDeleteButton {
cell.showDeleteButton()
} else {
cell.hideDeleteButton()
}
return cell
}
}
class SelectedMemberCell: UITableViewCell {
static let reuseIdentifier = "SelectedMemberCell"
weak var delegate: SelectedMemberCellProtocol?
private let avatarImageView: UIImageView = {
let imgView = UIImageView()
imgView.layer.cornerRadius = 2
imgView.layer.masksToBounds = true
return imgView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor.tui_color(withHex: "22262E")
label.textAlignment = isRTL ? .right : .left
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
label.numberOfLines = 1
return label
}()
let deleteButton: UIButton = {
let button = LargerHitAreaButton()
let image = UIImage(named: "room_delete", in: tuiRoomKitBundle(), compatibleWith: nil)
button.setImage(image, for: .normal)
return button
}()
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
selectionStyle = .none
constructViewHierarchy()
activateConstraints()
bindInteraction()
contentView.backgroundColor = .clear
}
private func constructViewHierarchy() {
contentView.addSubview(avatarImageView)
contentView.addSubview(nameLabel)
contentView.addSubview(deleteButton)
}
private func activateConstraints() {
avatarImageView.snp.makeConstraints { make in
make.width.height.equalTo(32)
make.leading.equalToSuperview().offset(20)
make.centerY.equalToSuperview()
}
nameLabel.snp.makeConstraints { make in
make.leading.equalTo(self.avatarImageView.snp.trailing).offset(5)
make.centerY.equalToSuperview()
}
deleteButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20)
make.centerY.equalToSuperview()
make.width.height.equalTo(16)
}
}
func updateView(with info: UserInfo) {
let placeholder = UIImage(named: "room_default_avatar_rect", in: tuiRoomKitBundle(), compatibleWith: nil)
if let url = URL(string: info.avatarUrl) {
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
} else {
avatarImageView.image = placeholder
}
if !info.userName.isEmpty {
nameLabel.text = info.userName
} else {
nameLabel.text = info.userId
}
}
func hideDeleteButton() {
self.deleteButton.isHidden = true
}
func showDeleteButton() {
self.deleteButton.isHidden = false
}
private func bindInteraction() {
deleteButton.addTarget(self, action: #selector(deleteButtonTapped(sender:)), for: .touchUpInside)
}
@objc func deleteButtonTapped(sender: UIButton) {
self.delegate?.didDeleteButtonClicked(in: self)
}
}
class SelectedMemberHeaderView: UITableViewHeaderFooterView {
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
backgroundView = UIView()
backgroundView?.backgroundColor = .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private let label: UILabel = {
let label = UILabel()
label.textColor = UIColor.tui_color(withHex: "4F586B")
label.font = UIFont(name: "PingFangSC-Medium", size: 18)
label.text = .selectedText
label.sizeToFit()
return label
}()
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
}
private func constructViewHierarchy() {
addSubview(label)
}
private func activateConstraints() {
label.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20)
make.top.equalToSuperview()
}
}
func updateLabel(with count: Int) {
let text = .selectedText + " (" + "\(count)" + ")"
self.label.text = text
}
}
class LargerHitAreaButton: UIButton {
var hitAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let largerFrame = bounds.inset(by: hitAreaEdgeInsets)
return largerFrame.contains(point)
}
}
private extension String {
static var selectedText: String {
localized("Selected")
}
}

View File

@@ -0,0 +1,23 @@
//
// SelectMemberViewFactory.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/6/18.
//
import Foundation
import RTCRoomEngine
struct MemberSelectParams {
let participants: ConferenceParticipants
let delegate: ContactViewSelectDelegate
let factory: MemberSelectionFactory
}
@objc public protocol ContactViewProtocol: AnyObject {
var delegate: ContactViewSelectDelegate? { get set }
}
@objc public protocol ContactViewSelectDelegate: AnyObject {
func onMemberSelected(_ viewController: ContactViewProtocol, invitees: [User])
}

View File

@@ -0,0 +1,123 @@
//
// ModifyScheduleDataHelper.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/27.
//
import Foundation
import RTCRoomEngine
class ModifyScheduleDataHelper: ScheduleConferenceDataHelper {
class func generateScheduleConferenceData(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore: ScheduleConferenceStore, viewController: ContactViewSelectDelegate) ->
[Int : [CellConfigItem]] {
var menus: [Int:[CellConfigItem]] = [:]
menus[0] = getFirstSectionModifyMenus(route: route, store: modifyStore, viewController: viewController)
menus[1] = getSecondSectionModifyMenus(route: route, store: store, operation: operation, modifyStore: modifyStore)
return menus
}
private class func getFirstSectionModifyMenus(route: Route, store: ScheduleConferenceStore, viewController: ContactViewSelectDelegate) -> [CellConfigItem] {
var array: [CellConfigItem] = []
array.append(getConferenceNameItem(route: route, store: store))
var conferenceTypeItem = getConferenceTypeItem(route: route, store: store)
conferenceTypeItem.showButton = false
conferenceTypeItem.selectClosure = nil
array.append(conferenceTypeItem)
array.append(getModifyStartTimeItem(route: route, store: store))
array.append(getModifyDurationTimeItem(route: route, store: store))
array.append(getTimeZoneItem(route: route, store: store))
array.append(getParticipatingMembersItem(route: route, store: store, viewController: viewController))
return array
}
private class func getSecondSectionModifyMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore:
ScheduleConferenceStore) -> [CellConfigItem] {
return [getSaveItem(route: route, store: store, operation: operation, modifyStore: modifyStore)]
}
private class func getModifyStartTimeItem(route: Route, store: ScheduleConferenceStore) -> CellConfigItem {
var startTimeItem = getStartTimeItem(route: route, store: store)
startTimeItem.selectClosure = {
let view = TimePickerView()
view.pickerDate = Date(timeIntervalSince1970: TimeInterval(store.conferenceInfo.scheduleStartTime))
view.dismissAction = {
route.dismiss(animated: true)
}
route.present(route: .popup(view: view))
}
return startTimeItem
}
private class func getModifyDurationTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
var durationTimeItem = getDurationTimeItem(route: route, store: store)
durationTimeItem.selectClosure = {
let view = DurationPickerView()
view.timeDuration = TimeInterval(store.conferenceInfo.durationTime)
view.dismissAction = {
route.dismiss(animated: true)
}
route.present(route: .popup(view: view))
}
return durationTimeItem
}
private class func getSaveItem(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore: ScheduleConferenceStore) -> CellConfigItem {
var item = ButtonItem(title: .saveText)
item.titleColor = UIColor(0xFFFFFF)
item.backgroudColor = UIColor(0x1C66E5)
item.selectClosure = {
guard modifyStore.conferenceInfo.basicInfo.name.count > 0 else {
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .nameCannotBeEmptyText)))
return
}
let currentList = operation.selectCurrent(ConferenceListSelectors.getConferenceList)
let status = currentList.first(where: { $0.basicInfo.roomId == store.conferenceInfo.basicInfo.roomId })?.status
guard status != .running else {
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .conferenceCannotBeModifiedText)))
route.pop()
return
}
updateConferenceInfoIfNeeded(store: store, operation: operation, modifyStore: modifyStore)
updateAttendsIfNeeded(store: store, operation: operation, modifyStore: modifyStore)
store.update(conference: modifyStore.conferenceInfo)
route.pop()
}
return item
}
private class func updateConferenceInfoIfNeeded(store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore: ScheduleConferenceStore) {
var modifyFlag: TUIConferenceModifyFlag = []
if modifyStore.conferenceInfo.basicInfo.name != store.conferenceInfo.basicInfo.name {
modifyFlag = modifyFlag.union(.roomName)
}
if modifyStore.conferenceInfo.scheduleStartTime != store.conferenceInfo.scheduleStartTime ||
modifyStore.conferenceInfo.durationTime != store.conferenceInfo.durationTime {
modifyFlag = modifyFlag.union([.scheduleStartTime, .scheduleEndTime])
}
if !modifyFlag.isEmpty {
let info = TUIConferenceInfo(conferenceInfo: modifyStore.conferenceInfo)
operation.dispatch(action: ConferenceListActions.updateConferenceInfo(payload: (info, modifyFlag)))
}
}
private class func updateAttendsIfNeeded(store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore: ScheduleConferenceStore){
let conferenceId = store.conferenceInfo.basicInfo.roomId
let newAttendeeSet = Set(modifyStore.conferenceInfo.attendeeListResult.attendeeList)
let oldAttendeeSet = Set(store.conferenceInfo.attendeeListResult.attendeeList)
let addList = newAttendeeSet.subtracting(oldAttendeeSet).map { $0.userId }
let removeList = oldAttendeeSet.subtracting(newAttendeeSet).map { $0.userId }
if !addList.isEmpty {
operation.dispatch(action: ConferenceListActions.addAttendeesByAdmin(payload: (conferenceId, addList)))
}
if !removeList.isEmpty {
operation.dispatch(action: ConferenceListActions.removeAttendeesByAdmin(payload: (conferenceId, removeList)))
}
}
}
private extension String {
static let saveText = localized("Save")
static let nameCannotBeEmptyText = localized("Conference name cannot be empty!")
static let conferenceCannotBeModifiedText = localized("Conference has already started, and it cannot be modified!")
}

View File

@@ -0,0 +1,101 @@
//
// ModifyScheduleViewController.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/26.
//
import Foundation
import Factory
import Combine
import TUICore
class ModifyScheduleViewController: UIViewController {
private var cancellableSet = Set<AnyCancellable>()
var conferenceInfo: ConferenceInfo
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
init(conferenceInfo: ConferenceInfo) {
self.conferenceInfo = conferenceInfo
super.init(nibName: nil, bundle: nil)
modifyStore.update(conference: self.conferenceInfo)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = ScheduleConferenceTableView(menus: ModifyScheduleDataHelper.generateScheduleConferenceData(route: route, store: store, operation: operation, modifyStore: modifyStore, viewController: self))
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = .modifyRoomText
subscribeScheduleSubject()
subscribeToast()
}
deinit {
debugPrint("deinit \(self)")
}
@Injected(\.navigation) private var route
@Injected(\.scheduleStore) private var store
@Injected(\.conferenceStore) private var operation
@Injected(\.modifyScheduleStore) private var modifyStore
}
extension ModifyScheduleViewController {
private func subscribeScheduleSubject() {
operation.scheduleActionSubject
.receive(on: RunLoop.main)
.filter { $0.id == ScheduleResponseActions.onUpdateInfoSuccess.id }
.sink { [weak self] action in
guard let self = self else { return }
self.route.pop()
}
.store(in: &cancellableSet)
}
private func subscribeToast() {
operation.toastSubject
.receive(on: DispatchQueue.main)
.sink { [weak self] toast in
guard let self = self else { return }
var position = TUICSToastPositionBottom
switch toast.position {
case .center:
position = TUICSToastPositionCenter
default:
break
}
self.view.makeToast(toast.message, duration: toast.duration, position: position)
}
.store(in: &cancellableSet)
}
}
extension ModifyScheduleViewController: ContactViewSelectDelegate {
public func onMemberSelected(_ viewController: ContactViewProtocol,
invitees: [User]) {
var conferenceInfo = modifyStore.conferenceInfo
conferenceInfo.attendeeListResult.attendeeList = invitees.map { $0.userInfo }
conferenceInfo.attendeeListResult.totalCount = UInt(invitees.count)
modifyStore.update(conference: conferenceInfo)
route.pop()
}
}
private extension String {
static var modifyRoomText: String {
localized("Modify Room")
}
}

View File

@@ -0,0 +1,23 @@
//
// ScheduleConference+Injection.swift
// TUIRoomKit
//
// Created by aby on 2024/6/27.
//
import Factory
extension Container {
var scheduleStore: Factory<ScheduleConferenceStore> {
self {
ScheduleConferenceStoreProvider()
}
.shared
}
var modifyScheduleStore: Factory<ScheduleConferenceStore> {
self {
ScheduleConferenceStoreProvider()
}
.shared
}
}

View File

@@ -0,0 +1,108 @@
//
// ScheduleConferenceStore.swift
// TUIRoomKit
//
// Created by aby on 2024/6/27.
//
import Combine
protocol ScheduleConferenceStore {
func fetchAttendees(cursor: String)
func update(conference info: ConferenceInfo)
func fetchRoomInfo(roomId: String)
func select<Value:Equatable>(_ selector: Selector<ConferenceInfo, Value>) -> AnyPublisher<Value, Never>
var conferenceInfo: ConferenceInfo { get }
}
class ScheduleConferenceStoreProvider {
static let updateConferenceInfo = ActionTemplate(id: "updateConferenceInfo", payloadType: ConferenceInfo.self)
static let fetchAttendeeList = ActionTemplate(id: "fetchAttendeeList", payloadType: (String, String, Int).self)
static let updateAttendeeList = ActionTemplate(id: "updateAttendeeList", payloadType: ([UserInfo], String, UInt).self)
static let updateBasicInfo = ActionTemplate(id: "updateBasicInfo", payloadType: RoomInfo.self)
static let fetchRoomInfo = ActionTemplate(id: ".fetchRoomInfo", payloadType: String.self)
static let attendeesPerFetch = 20
// MARK: - private property.
private lazy var store: Store<ConferenceInfo, ServiceCenter> = {
let store = Store.init(initialState: ConferenceInfo(), environment: ServiceCenter(), reducers: [self.conferenceReducer])
store.register(effects: scheduleConferenceEffects())
return store
}()
private let conferenceReducer = Reducer<ConferenceInfo>(
ReduceOn(updateConferenceInfo, reduce: { state, action in
state = action.payload
}),
ReduceOn(updateAttendeeList, reduce: { state, action in
state.attendeeListResult.attendeeList.append(contentsOf: action.payload.0)
state.attendeeListResult.fetchCursor = action.payload.1
state.attendeeListResult.totalCount = action.payload.2
}),
ReduceOn(updateBasicInfo, reduce: { state, action in
state.basicInfo = action.payload
})
)
deinit {
store.unregister(reducer: conferenceReducer)
store.unregisterEffects(withId: scheduleConferenceEffects.id)
}
}
extension ScheduleConferenceStoreProvider: ScheduleConferenceStore {
func fetchAttendees(cursor: String) {
let conferenceId = conferenceInfo.basicInfo.roomId
store.dispatch(action: ScheduleConferenceStoreProvider.fetchAttendeeList(payload: (conferenceId, cursor, ScheduleConferenceStoreProvider.attendeesPerFetch)))
}
func update(conference info: ConferenceInfo) {
store.dispatch(action: ScheduleConferenceStoreProvider.updateConferenceInfo(payload: info))
}
func fetchRoomInfo(roomId: String) {
store.dispatch(action: ScheduleConferenceStoreProvider.fetchRoomInfo(payload: roomId))
}
func select<Value>(_ selector: Selector<ConferenceInfo, Value>) -> AnyPublisher<Value, Never> where Value : Equatable {
return store.select(selector)
}
var conferenceInfo: ConferenceInfo {
return store.state
}
}
class scheduleConferenceEffects: Effects {
typealias Environment = ServiceCenter
let fetchAttendeeList = Effect<Environment>.dispatchingOne { actions, environment in
actions.wasCreated(from: ScheduleConferenceStoreProvider.fetchAttendeeList)
.flatMap { action in
environment.conferenceListService.fetchAttendeeList(conferenceId: action.payload.0,
cursor: action.payload.1,
count: action.payload.2)
.map { (userInfoList, cursor, totalCount) in
ScheduleConferenceStoreProvider.updateAttendeeList(payload: (userInfoList, cursor, totalCount))
}
.catch { error -> Just<Action> in
Just(ErrorActions.throwError(payload: error))
}
}
.eraseToAnyPublisher()
}
let fetchRoomInfo = Effect<Environment>.dispatchingOne { actions, environment in
actions.wasCreated(from: ScheduleConferenceStoreProvider.fetchRoomInfo)
.flatMap { action in
environment.conferenceListService.fetchConferenceInfo(roomId: action.payload)
.map { conferenceInfo in
ScheduleConferenceStoreProvider.updateBasicInfo(payload: conferenceInfo.basicInfo)
}
.catch { error -> Just<Action> in
Just(ErrorActions.throwError(payload: error))
}
}
.eraseToAnyPublisher()
}
}

View File

@@ -0,0 +1,357 @@
//
// ScheduleConferenceDataHelper.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/19.
//
import Foundation
import RTCRoomEngine
class ScheduleConferenceDataHelper {
open class func generateScheduleConferenceData(route: Route,
store: ScheduleConferenceStore,
operation: ConferenceStore,
viewController: ContactViewSelectDelegate? = nil) -> [Int: [CellConfigItem]] {
var menus: [Int:[CellConfigItem]] = [:]
menus[0] = getFirstSectionMenus(route: route, store: store, viewController: viewController)
menus[1] = getSecondSectionMenus(route: route, store: store)
menus[2] = getThirdSectionMenus(route: route, store: store)
menus[3] = getFourthSectionMenus(route: route, store: store, operation: operation)
return menus
}
}
// MARK: - private function.
extension ScheduleConferenceDataHelper {
class func getFirstSectionMenus(route: Route, store: ScheduleConferenceStore, viewController: ContactViewSelectDelegate?) -> [CellConfigItem] {
var array: [CellConfigItem] = []
array.append(getConferenceNameItem(route: route, store: store))
array.append(getConferenceTypeItem(route: route, store: store))
array.append(getStartTimeItem(route: route, store: store))
array.append(getDurationTimeItem(route: route, store: store))
array.append(getTimeZoneItem(route: route, store: store))
array.append(getParticipatingMembersItem(route: route, store: store, viewController: viewController))
return array
}
class func getSecondSectionMenus(route: Route, store: ScheduleConferenceStore) -> [CellConfigItem] {
var array: [CellConfigItem] = []
array.append(getEncryptRoomItem(store: store))
if store.conferenceInfo.basicInfo.isPasswordEnabled {
array.append(getRoomPasswordItem(store: store))
}
return array
}
class func getThirdSectionMenus(route: Route, store: ScheduleConferenceStore) -> [CellConfigItem] {
var array: [CellConfigItem] = []
array.append(getMuteAllItem(route: route, store: store))
array.append(getFreezeVideoItem(route: route, store: store))
return array
}
class func getFourthSectionMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> [CellConfigItem] {
return [getbookItem(route: route, store: store, operation: operation)]
}
class func getConferenceNameItem(route: Route, store: ScheduleConferenceStore) -> TextFieldItem{
var conferenceNameItem = TextFieldItem(title: .roomNameText, content: store.conferenceInfo.basicInfo.name)
conferenceNameItem.saveTextClosure = { text in
var conferenceInfo = store.conferenceInfo
conferenceInfo.basicInfo.name = text
store.update(conference: conferenceInfo)
}
conferenceNameItem.bindStateClosure = { cell, cancellableSet in
let getBasicInfo = Selector(keyPath: \ConferenceInfo.basicInfo)
let selector = Selector.with(getBasicInfo, keyPath: \RoomInfo.name)
store.select(selector)
.receive(on: RunLoop.main)
.sink { [weak cell] text in
if let cell = cell as? TextFieldCell {
cell.textField.text = text
}
}
.store(in: &cancellableSet)
}
return conferenceNameItem
}
class func getConferenceTypeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
let enableSeatControl = store.conferenceInfo.basicInfo.isSeatEnabled
var conferenceTypeItem = ListItem(title: .roomTypeText, content: enableSeatControl ? .onStageSpeechRoomText: .freeSpeechRoomText)
conferenceTypeItem.showButton = true
conferenceTypeItem.selectClosure = {
let view = RoomTypeView()
view.dismissAction = {
route.dismiss(animated: true)
}
route.present(route: .popup(view: view))
}
conferenceTypeItem.bindStateClosure = { cell, cancellableSet in
let getBasicInfo = Selector(keyPath: \ConferenceInfo.basicInfo)
let selector = Selector.with(getBasicInfo, keyPath: \RoomInfo.isSeatEnabled)
store.select(selector)
.receive(on: RunLoop.main)
.sink { [weak cell] enableSeatControl in
if let cell = cell as? ScheduleTabCell {
cell.messageLabel.text = enableSeatControl ? .onStageSpeechRoomText: .freeSpeechRoomText
}
}
.store(in: &cancellableSet)
}
return conferenceTypeItem
}
class func getStartTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
let startTime = TimeInterval(store.conferenceInfo.scheduleStartTime)
var startTimeItem = ListItem(title: .startingTimeText, content: getTimeIntervalString(startTime, timeZone: store.conferenceInfo.timeZone))
startTimeItem.showButton = true
startTimeItem.selectClosure = {
let view = TimePickerView()
view.pickerDate = Date(timeIntervalSince1970: TimeInterval(store.conferenceInfo.scheduleStartTime))
view.dismissAction = {
route.dismiss(animated: true)
}
route.present(route: .popup(view: view))
}
startTimeItem.bindStateClosure = { cell, cancellableSet in
let selector = Selector(keyPath: \ConferenceInfo.scheduleStartTime)
store.select(selector)
.receive(on: RunLoop.main)
.sink { [weak cell] startTime in
if let cell = cell as? ScheduleTabCell {
cell.messageLabel.text = getTimeIntervalString(TimeInterval(startTime), timeZone: store.conferenceInfo.timeZone)
}
}
.store(in: &cancellableSet)
}
return startTimeItem
}
class func getDurationTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
var durationTimeItem = ListItem(title: .roomDurationText, content: getDurationTimeString(store.conferenceInfo.durationTime))
durationTimeItem.showButton = true
durationTimeItem.selectClosure = {
let view = DurationPickerView()
view.dismissAction = {
route.dismiss(animated: true)
}
route.present(route: .popup(view: view))
}
durationTimeItem.bindStateClosure = { cell, cancellableSet in
let selector = Selector(keyPath: \ConferenceInfo.durationTime)
store.select(selector)
.receive(on: RunLoop.main)
.sink { durationTime in
if let cell = cell as? ScheduleTabCell {
cell.messageLabel.text = getDurationTimeString(durationTime)
}
}
.store(in: &cancellableSet)
}
return durationTimeItem
}
class func getTimeZoneItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
var timeZoneItem = ListItem(title: .rimeZoneText, content: store.conferenceInfo.timeZone.getTimeZoneName())
timeZoneItem.showButton = true
timeZoneItem.buttonIcon = "room_right_arrow1"
timeZoneItem.selectClosure = {
route.pushTo(route: .timeZone)
}
timeZoneItem.bindStateClosure = { cell, cancellableSet in
let selector = Selector(keyPath: \ConferenceInfo.timeZone)
store.select(selector)
.receive(on: RunLoop.main)
.sink { timeZone in
if let cell = cell as? ScheduleTabCell {
cell.messageLabel.text = timeZone.getTimeZoneName()
}
}
.store(in: &cancellableSet)
}
return timeZoneItem
}
class func getParticipatingMembersItem(route: Route, store: ScheduleConferenceStore, viewController: ContactViewSelectDelegate? = nil) -> ListItem {
var participatingMembersItem = ListItem(title: .participatingMembersText)
participatingMembersItem.showButton = true
participatingMembersItem.buttonIcon = "room_right_arrow1"
participatingMembersItem.selectClosure = { [weak viewController] in
guard let vc = viewController else { return }
let users = store.conferenceInfo.attendeeListResult.attendeeList.map { $0.convertToUser() }
let participants = ConferenceParticipants(selectedList: users)
route.showContactView(delegate: vc, participants: participants)
}
participatingMembersItem.bindStateClosure = { cell, cancellableSet in
let selector = Selector(keyPath: \ConferenceInfo.attendeeListResult.attendeeList)
store.select(selector)
.receive(on: RunLoop.main)
.sink { list in
if let cell = cell as? ScheduleTabCell {
var iconList: [String] = []
for i in 0...2 {
if let userInfo = list[safe: i] {
let avatarUrl = userInfo.avatarUrl.count > 0 ? userInfo.avatarUrl : "room_default_avatar_rect"
iconList.append(avatarUrl)
}
}
cell.updateStackView(iconList: iconList)
let totalCount = store.conferenceInfo.attendeeListResult.totalCount
cell.messageLabel.text = totalCount == 0 ? .addToText : localizedReplace(.participantsNumber, replace: String(totalCount))
}
}
.store(in: &cancellableSet)
}
return participatingMembersItem
}
class func getEncryptRoomItem(store: ScheduleConferenceStore) -> SwitchItem {
var encryptRoomItem = SwitchItem(title: .encryptTheRoomText)
encryptRoomItem.isOn = store.conferenceInfo.basicInfo.isPasswordEnabled
encryptRoomItem.selectClosure = {
var conferenceInfo = store.conferenceInfo
conferenceInfo.basicInfo.isPasswordEnabled = !store.conferenceInfo.basicInfo.isPasswordEnabled
store.update(conference: conferenceInfo)
}
return encryptRoomItem
}
class func getRoomPasswordItem(store: ScheduleConferenceStore) -> TextFieldItem {
var roomPasswordItem = TextFieldItem(title: .roomPasswordText, content: store.conferenceInfo.basicInfo.password)
roomPasswordItem.keyboardType = .numberPad
roomPasswordItem.maxLengthInBytes = 6
roomPasswordItem.placeholder = .enterJoinRoomPassword
roomPasswordItem.saveTextClosure = { text in
var conferenceInfo = store.conferenceInfo
conferenceInfo.basicInfo.password = text
store.update(conference: conferenceInfo)
}
return roomPasswordItem
}
class func getMuteAllItem(route: Route, store: ScheduleConferenceStore) -> SwitchItem {
var muteAllItem = SwitchItem(title: .muteAllText)
muteAllItem.isOn = store.conferenceInfo.basicInfo.isMicrophoneDisableForAllUser
muteAllItem.selectClosure = {
var conferenceInfo = store.conferenceInfo
conferenceInfo.basicInfo.isMicrophoneDisableForAllUser = !conferenceInfo.basicInfo.isMicrophoneDisableForAllUser
store.update(conference: conferenceInfo)
}
muteAllItem.bindStateClosure = { cell, cancellableSet in
let selector = Selector(keyPath: \ConferenceInfo.basicInfo.isMicrophoneDisableForAllUser)
store.select(selector)
.receive(on: RunLoop.main)
.sink { isMicrophoneDisableForAllUser in
if let cell = cell as? SwitchCell {
cell.rightSwitch.isOn = isMicrophoneDisableForAllUser
}
}
.store(in: &cancellableSet)
}
return muteAllItem
}
class func getFreezeVideoItem(route: Route, store: ScheduleConferenceStore) -> SwitchItem {
var freezeVideoItem = SwitchItem(title: .freezeVideoText)
freezeVideoItem.isOn = store.conferenceInfo.basicInfo.isCameraDisableForAllUser
freezeVideoItem.selectClosure = {
var conferenceInfo = store.conferenceInfo
conferenceInfo.basicInfo.isCameraDisableForAllUser = !conferenceInfo.basicInfo.isCameraDisableForAllUser
store.update(conference: conferenceInfo)
}
freezeVideoItem.bindStateClosure = { cell, cancellableSet in
let selector = Selector(keyPath: \ConferenceInfo.basicInfo.isCameraDisableForAllUser)
store.select(selector)
.receive(on: RunLoop.main)
.sink { isCameraDisableForAllUser in
if let cell = cell as? SwitchCell {
cell.rightSwitch.isOn = isCameraDisableForAllUser
}
}
.store(in: &cancellableSet)
}
return freezeVideoItem
}
class func getbookItem(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> ButtonItem {
var bookItem = ButtonItem(title: .bookRoomText)
bookItem.titleColor = UIColor(0xFFFFFF)
bookItem.backgroudColor = UIColor(0x1C66E5)
bookItem.selectClosure = {
guard TimeInterval(store.conferenceInfo.scheduleStartTime) >= Date().timeIntervalSince1970 else {
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .startTimeCannotEarlierCurrentTime)))
return
}
guard store.conferenceInfo.basicInfo.name.count > 0 else {
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .nameCannotBeEmptyText)))
return
}
guard checkPasswordFormat(conferenceInfo: store.conferenceInfo) else {
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .passwordFormatIsIncorrect)))
return
}
let conferenceInfo = TUIConferenceInfo(conferenceInfo: store.conferenceInfo)
operation.dispatch(action: ConferenceListActions.scheduleConference(payload: conferenceInfo))
}
return bookItem
}
class func checkPasswordFormat(conferenceInfo: ConferenceInfo) -> Bool {
if conferenceInfo.basicInfo.isPasswordEnabled {
let password = conferenceInfo.basicInfo.password
let passwordLength = 6
return password.count == passwordLength && password.isStringOnlyDigits()
} else {
return true
}
}
class func getTimeIntervalString(_ time: TimeInterval, timeZone: TimeZone) -> String {
let date = Date(timeIntervalSince1970: time)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd HH:mm"
dateFormatter.timeZone = timeZone
return dateFormatter.string(from: date)
}
class func getDurationTimeString(_ time: UInt) -> String {
guard time > 0 else { return "" }
let hour = time / 3_600
let minute = (time / 60) % 60
var text = ""
if hour > 0 {
text = String(hour) + .hour
}
if minute > 0 {
text = text + String(minute) + .minute
}
return text
}
}
private extension String {
static let roomNameText = localized("Room name")
static let roomTypeText = localized("Room type")
static let startingTimeText = localized("Starting time")
static let roomDurationText = localized("Room duration")
static let rimeZoneText = localized("Time zone")
static let freeSpeechRoomText = localized("Free Speech Room")
static let onStageSpeechRoomText = localized("On-stage Speech Room")
static let muteAllText = localized("Mute All")
static let freezeVideoText = localized("Freeze video")
static let hour = localized("hour")
static let minute = localized("minute")
static let participatingMembersText = localized("Participating members")
static let addToText = localized("Add to")
static let encryptTheRoomText = localized("Encrypt the room")
static let roomPasswordText = localized("Room Password")
static let bookRoomText = localized("Schedule Room")
static let nameCannotBeEmptyText = localized("Conference name cannot be empty!")
static let participantsNumber = localized("xx/300 people")
static let passwordFormatIsIncorrect = localized("Your room password format is incorrect, please check it")
static let enterJoinRoomPassword = localized("Enter 6-digit password")
static let startTimeCannotEarlierCurrentTime = localized("The start time cannot be earlier than the current time")
}

View File

@@ -0,0 +1,137 @@
//
// ScheduleConferenceTableView.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/24.
//
import Foundation
import Factory
class ScheduleConferenceTableView: UIView {
var menus: [Int: [CellConfigItem]]
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = .clear
tableView.sectionFooterHeight = 20.scale375Height()
tableView.sectionHeaderHeight = 0
tableView.register(ScheduleTabCell.self, forCellReuseIdentifier: ScheduleTabCell.identifier)
tableView.register(SwitchCell.self, forCellReuseIdentifier: SwitchCell.identifier)
tableView.register(TextFieldCell.self, forCellReuseIdentifier: TextFieldCell.identifier)
tableView.register(ButtonCell.self, forCellReuseIdentifier: ButtonCell.identifier)
return tableView
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
isViewReady = true
constructViewHierarchy()
activateConstraints()
backgroundColor = UIColor(0xF8F9FB)
}
init(menus: [Int : [CellConfigItem]]) {
self.menus = menus
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func constructViewHierarchy() {
addSubview(tableView)
}
private func activateConstraints() {
tableView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.bottom.equalToSuperview().offset(-10.scale375Height())
}
}
}
extension ScheduleConferenceTableView: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return menus.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let value = menus[section] else { return 0 }
return value.count
}
}
extension ScheduleConferenceTableView: UITableViewDelegate {
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let items = menus[indexPath.section] ?? []
let item = items[indexPath.row]
let identifier = item.cellType.cellIdentifier
let cell = tableView.dequeueReusableCell(withIdentifier:identifier, for: indexPath)
if let scheduleTabCell = cell as? ScheduleTabCell {
scheduleTabCell.updateView(item: item)
} else if let switchCell = cell as? SwitchCell {
switchCell.updateView(item: item)
} else if let textFieldCell = cell as? TextFieldCell {
textFieldCell.updateView(item: item)
} else if let buttonCell = cell as? ButtonCell {
buttonCell.updateView(item: item)
}
if let baseCell = cell as? ScheduleBaseCell {
item.bindStateClosure?(baseCell, &baseCell.cancellableSet)
}
cell.selectionStyle = .none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let items = menus[indexPath.section] ?? []
let item = items[indexPath.row]
let cell = tableView.cellForRow(at: indexPath)
if cell is ScheduleTabCell {
item.selectClosure?()
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let normalHeight = 45.scale375Height()
if indexPath.section == 0 {
return normalHeight
}
guard let itemArray = menus[indexPath.section], let item = itemArray[safe: indexPath.item] else { return normalHeight }
switch item.cellType {
case .switcher, .textField:
return 54.scale375Height()
case .button:
return 44.scale375Height()
default:
return normalHeight
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let rows = tableView.numberOfRows(inSection: indexPath.section)
if indexPath.row == 0 || indexPath.row == rows - 1 {
var corner = UIRectCorner()
if rows == 1 {
corner = .allCorners
} else if indexPath.row == 0 {
corner = [.topLeft, .topRight]
} else if indexPath.row == rows - 1 {
corner = [.bottomLeft, .bottomRight]
}
cell.roundedRect(rect: cell.bounds,
byRoundingCorners: corner,
cornerRadii: CGSize(width: 12, height: 12))
}
}
}

View File

@@ -0,0 +1,194 @@
//
// ScheduleDetailsDataHelper.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/26.
//
import Foundation
import RTCRoomEngine
class ScheduleDetailsDataHelper: ScheduleConferenceDataHelper {
class func generateScheduleDetailsConferenceData(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore, viewStore: ConferenceMainViewStore) -> [Int : [CellConfigItem]] {
var menus: [Int:[CellConfigItem]] = [:]
menus[0] = getFirstSectionDetailsMenus(route: route, store: store, operation: operation)
menus[1] = getSecondSectionDetailsMenus(store: store, operation: operation, viewStore: viewStore)
menus[2] = getThirdSectionDetailsMenus(route: route, store: store, operation: operation)
guard let fourthSectionDetailsMenus = getFourthSectionDetailsMenus(route: route, store: store, operation: operation) else { return menus }
menus[3] = fourthSectionDetailsMenus
return menus
}
private class func getFirstSectionDetailsMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> [CellConfigItem] {
var array: [CellConfigItem] = []
array.append(getDetailsConferenceNameItem(route: route, store: store))
array.append(getDetailsConferenceIdItem(route: route, store: store, operation: operation))
array.append(getDetailsStartTimeItem(route: route, store: store))
array.append(getDetailsDurationTimeItem(route: route, store: store))
array.append(getDetailsConferenceTypeItem(route: route, store: store))
if let passwordItem = getConferencePasswordItem(store: store) {
array.append(passwordItem)
}
array.append(getRoomHostItem(route: route, store: store))
array.append(getDetailsParticipatingMembersItem(route: route, store: store))
return array
}
private class func getSecondSectionDetailsMenus(store: ScheduleConferenceStore, operation: ConferenceStore, viewStore: ConferenceMainViewStore) -> [CellConfigItem] {
return [getEnterRoomItem(store: store, operation: operation, viewStore: viewStore)]
}
private class func getThirdSectionDetailsMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> [CellConfigItem] {
return [getInviteItem(route: route, store: store)]
}
private class func getFourthSectionDetailsMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> [CellConfigItem]? {
guard store.conferenceInfo.basicInfo.ownerId == operation.selectCurrent(UserSelectors.getSelfId) else { return nil }
guard store.conferenceInfo.status == .notStarted else { return nil }
return [getCancelRoomItem(route: route, store: store, operation: operation)]
}
private class func getDetailsConferenceNameItem(route: Route, store: ScheduleConferenceStore) -> TextFieldItem {
var conferenceNameItem = getConferenceNameItem(route: route, store: store)
conferenceNameItem.isEnable = false
return conferenceNameItem
}
private class func getDetailsConferenceIdItem(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> ListItem {
var conferenceIdItem = ListItem(title: .roomIDText, content: store.conferenceInfo.basicInfo.roomId)
conferenceIdItem.showButton = true
conferenceIdItem.buttonIcon = "room_copy_blue"
conferenceIdItem.selectClosure = {
UIPasteboard.general.string = store.conferenceInfo.basicInfo.roomId
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .copyRoomIdSuccess)))
}
return conferenceIdItem
}
private class func getDetailsConferenceTypeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
var conferenceTypeItem = getConferenceTypeItem(route: route, store: store)
conferenceTypeItem.showButton = false
conferenceTypeItem.selectClosure = nil
return conferenceTypeItem
}
private class func getDetailsStartTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
var startTimeItem = getStartTimeItem(route: route, store: store)
startTimeItem.selectClosure = nil
startTimeItem.showButton = false
return startTimeItem
}
private class func getDetailsDurationTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
var durationTimeItem = getDurationTimeItem(route: route, store: store)
durationTimeItem.selectClosure = nil
durationTimeItem.showButton = false
return durationTimeItem
}
private class func getRoomHostItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
var hostItem = ListItem(title: .creatorText, content: store.conferenceInfo.basicInfo.ownerName)
hostItem.iconList = [store.conferenceInfo.basicInfo.ownerAvatarUrl]
return hostItem
}
private class func getDetailsParticipatingMembersItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
var item = getParticipatingMembersItem(route: route, store: store)
item.buttonIcon = "room_down_arrow1"
item.selectClosure = {
if store.conferenceInfo.attendeeListResult.attendeeList.count > 0 {
route.present(route: .selectedMember(showDeleteButton: false, selectedMembers: store.conferenceInfo.attendeeListResult.attendeeList))
}
}
item.bindStateClosure = { cell, cancellableSet in
let selector = Selector(keyPath: \ConferenceInfo.attendeeListResult.attendeeList)
store.select(selector)
.receive(on: RunLoop.main)
.removeDuplicates()
.sink { list in
if let cell = cell as? ScheduleTabCell {
var iconList: [String] = []
for i in 0...2 {
if let userInfo = list[safe: i] {
let avatarUrl = userInfo.avatarUrl.count > 0 ? userInfo.avatarUrl : "room_default_avatar_rect"
iconList.append(avatarUrl)
}
}
cell.updateStackView(iconList: iconList)
let totalCount = store.conferenceInfo.attendeeListResult.totalCount
cell.messageLabel.text = totalCount == 0 ? .noParticipantsYet : localizedReplace(.participantsNumber, replace: String(totalCount))
cell.updateButton(isShown: list.count > 0)
}
}
.store(in: &cancellableSet)
}
return item
}
private class func getConferencePasswordItem(store: ScheduleConferenceStore) -> ListItem? {
guard store.conferenceInfo.basicInfo.password.count > 0 else { return nil }
var passwordItem = ListItem(title: .conferencePasswordText)
passwordItem.content = store.conferenceInfo.basicInfo.password
passwordItem.selectClosure = nil
passwordItem.showButton = false
return passwordItem
}
private class func getEnterRoomItem(store: ScheduleConferenceStore, operation: ConferenceStore, viewStore: ConferenceMainViewStore) -> ButtonItem {
var item = ButtonItem(title: .enterTheRoomText)
item.titleColor = UIColor(0x0961F7)
item.backgroudColor = UIColor(0xF0F3FA)
item.selectClosure = {
let conferenceId = store.conferenceInfo.basicInfo.roomId
operation.dispatch(action: RoomActions.joinConference(payload: conferenceId))
operation.dispatch(action: ScheduleViewActions.popDetailView())
viewStore.updateInternalCreation(isInternalCreation: true)
}
return item
}
private class func getInviteItem(route: Route, store: ScheduleConferenceStore) -> ButtonItem {
var item = ButtonItem(title: .inviteMemberText)
item.titleColor = UIColor(0x0961F7)
item.backgroudColor = UIColor(0xF0F3FA)
item.selectClosure = {
let view = InviteEnterRoomView(conferenceInfo: store.conferenceInfo)
route.present(route: .popup(view: view))
}
return item
}
private class func getCancelRoomItem(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> ButtonItem {
var item = ButtonItem(title: .cancelRoomText)
item.titleColor = UIColor(0xED414D)
item.backgroudColor = UIColor(0xFAF0F0)
item.selectClosure = {
let declineAction = UIAlertAction(title: .notCanceledYet, style: .cancel)
declineAction.setValue(UIColor(0x4F586B), forKey: "titleTextColor")
let sureAction = UIAlertAction(title: .cancelRoom, style: .default) { _ in
operation.dispatch(action: ConferenceListActions.cancelConference(payload: store.conferenceInfo.basicInfo.roomId))
}
sureAction.setValue(UIColor(0xED414D), forKey: "titleTextColor")
let alertState = AlertState(title: .cancelBookedRoomTitle, message: .cancelBookedRoomMessage, sureAction: sureAction, declineAction: declineAction)
route.present(route: .alert(state: alertState))
}
return item
}
}
private extension String {
static let creatorText: String = localized("Creator")
static let roomIDText: String = localized("Room ID")
static let enterTheRoomText = localized("Enter the room")
static let inviteMemberText = localized("Invite member")
static let cancelRoomText = localized("Cancel Room")
static let cancelBookedRoomTitle = localized("Cancel this booked room")
static let cancelBookedRoomMessage = localized("After cancellation, other members will not be able to join")
static let notCanceledYet = localized("Not canceled yet")
static let cancelRoom = localized("Cancel Room")
static let copyRoomIdSuccess = localized("Conference ID copied.")
static let noParticipantsYet = localized("No participants yet")
static let participantsNumber = localized("xx/300 people")
static let conferencePasswordText = localized("Conference password")
}

View File

@@ -0,0 +1,176 @@
//
// ScheduleDetailsViewController .swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/24.
//
import Foundation
import Factory
import Combine
import TUICore
import RTCRoomEngine
class ScheduleDetailsViewController: UIViewController {
var conferenceInfo: ConferenceInfo
private var cancellableSet = Set<AnyCancellable>()
private lazy var rootView: ScheduleConferenceTableView = {
return ScheduleConferenceTableView(menus: ScheduleDetailsDataHelper.generateScheduleDetailsConferenceData(route: route, store: store, operation: operation, viewStore: viewStore))
}()
private lazy var conferenceListPublisher = {
operation.select(ConferenceListSelectors.getConferenceList)
}()
init(conferenceInfo: ConferenceInfo) {
self.conferenceInfo = conferenceInfo
super.init(nibName: nil, bundle: nil)
store.update(conference: self.conferenceInfo)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override func loadView() {
self.view = rootView
}
lazy var modifyButton: UIButton = {
let button = UIButton()
button.setTitle(.reviseText, for: .normal)
button.setTitleColor(UIColor(0x1C66E5), for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
button.addTarget(self, action: #selector(modifyAction(sender:)), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
initState()
subscribeToast()
subscribeScheduleSubject()
navigationItem.title = .roomDetailsText
if store.conferenceInfo.basicInfo.ownerId == operation.selectCurrent(UserSelectors.getSelfId),
store.conferenceInfo.status == .notStarted {
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: modifyButton)
}
store.fetchAttendees(cursor: "")
let cursorSelector = Selector(keyPath: \ConferenceInfo.attendeeListResult.fetchCursor)
store.select(cursorSelector)
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink {[weak self] cursor in
guard let self = self else { return }
if !cursor.isEmpty {
self.store.fetchAttendees(cursor: cursor)
}
}
.store(in: &cancellableSet)
let selector = Selector(keyPath: \ConferenceInfo.basicInfo.isPasswordEnabled)
store.select(selector)
.removeDuplicates()
.receive(on: RunLoop.main)
.sink { [weak self] isPasswordEnabled in
guard let self = self else { return }
self.rootView.menus = ScheduleDetailsDataHelper.generateScheduleDetailsConferenceData(route: self.route, store: self.store, operation: self.operation, viewStore: self.viewStore)
self.rootView.tableView.reloadData()
}
.store(in: &cancellableSet)
operation.select(ViewSelectors.getPopDetailFlag)
.receive(on: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] shouldPopDetail in
guard let self = self else { return }
if shouldPopDetail {
self.route.pop(route: .scheduleDetails(conferenceInfo:self.conferenceInfo))
}
}
.store(in: &cancellableSet)
conferenceListPublisher
.receive(on: RunLoop.main)
.sink { [weak self] list in
guard let self = self else { return }
guard let selectedConferenceInfo = list.first(where: { $0.basicInfo.roomId == self.conferenceInfo.basicInfo.roomId }) else { return }
guard selectedConferenceInfo.status != self.store.conferenceInfo.status, selectedConferenceInfo.status == .running else { return }
self.modifyButton.isHidden = true
var conferenceInfo = self.store.conferenceInfo
conferenceInfo.status = selectedConferenceInfo.status
self.store.update(conference: conferenceInfo)
let menus = ScheduleDetailsDataHelper.generateScheduleDetailsConferenceData(route: self.route, store: self.store, operation: self.operation, viewStore: self.viewStore)
self.rootView.menus = menus
self.rootView.tableView.reloadData()
}
.store(in: &cancellableSet)
store.fetchRoomInfo(roomId: conferenceInfo.basicInfo.roomId)
}
private func initState() {
self.operation.dispatch(action: UserActions.getSelfInfo())
self.operation.dispatch(action: ScheduleViewActions.resetPopDetailFlag())
}
@objc func modifyAction(sender: UIButton) {
route.pushTo(route: .modifySchedule(conferenceInfo: store.conferenceInfo))
}
deinit {
debugPrint("deinit \(self)")
}
// MARK: - private property.
@Injected(\.navigation) private var route
@Injected(\.scheduleStore) private var store
@Injected(\.conferenceStore) private var operation
@Injected(\.conferenceMainViewStore) private var viewStore
}
extension ScheduleDetailsViewController {
private func subscribeToast() {
operation.toastSubject
.receive(on: DispatchQueue.main)
.sink { [weak self] toast in
guard let self = self else { return }
var position = TUICSToastPositionBottom
switch toast.position {
case .center:
position = TUICSToastPositionCenter
default:
break
}
if self.presentedViewController == nil {
self.view.makeToast(toast.message, duration: toast.duration, position: position)
}
}
.store(in: &cancellableSet)
}
private func subscribeScheduleSubject() {
operation.scheduleActionSubject
.receive(on: RunLoop.main)
.filter { $0.id == ScheduleResponseActions.onCancelSuccess.id }
.sink { [weak self] action in
guard let self = self else { return }
self.route.pop()
}
.store(in: &cancellableSet)
}
}
private extension String {
static let roomDetailsText = localized("Room Details")
static let reviseText = localized("Revise")
}

View File

@@ -0,0 +1,61 @@
//
// ButtonCell.swift
// TUIRoomKit
//
// Created by janejntang on 2024/7/5.
//
import Foundation
class ButtonCell: ScheduleBaseCell {
static let identifier = "ButtonCell"
var item: CellConfigItem?
let button: UIButton = {
let button = UIButton()
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
button.titleLabel?.textAlignment = .center
button.layer.cornerRadius = 12
return button
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
isViewReady = true
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
private func constructViewHierarchy() {
contentView.addSubview(button)
}
private func activateConstraints() {
button.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func bindInteraction() {
button.addTarget(self, action: #selector(buttonAction(sender:)), for: .touchUpInside)
}
@objc func buttonAction(sender: UIButton) {
item?.selectClosure?()
}
func updateView(item: CellConfigItem) {
self.item = item
guard let buttonItem = item as? ButtonItem else { return }
button.setTitle(buttonItem.title, for: .normal)
button.setTitleColor(buttonItem.titleColor, for: .normal)
button.backgroundColor = buttonItem.backgroudColor
}
deinit {
debugPrint("deinit:\(self)")
}
}

View File

@@ -0,0 +1,134 @@
//
// DurationPickerView.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/14.
//
import Foundation
import Factory
class DurationPickerView: UIView {
var dismissAction: (() -> Void)?
var timeDuration: TimeInterval = 1800
let topView: UIView = {
let view = UIView()
return view
}()
let topLabel: UILabel = {
let label = UILabel()
label.text = .roomDurationText
label.font = UIFont.systemFont(ofSize: 16, weight: .heavy)
label.textColor = UIColor(0x22262E)
return label
}()
let cancelButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "schedule_wrong", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
return button
}()
let sureButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "schedule_right", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
return button
}()
let timePickerView: UIDatePicker = {
let pickerView = UIDatePicker()
pickerView.datePickerMode = .countDownTimer
if #available(iOS 13.4, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.minuteInterval = 5
return pickerView
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
backgroundColor = UIColor(0xFFFFFF)
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(topView)
topView.addSubview(topLabel)
topView.addSubview(cancelButton)
topView.addSubview(sureButton)
addSubview(timePickerView)
}
func activateConstraints() {
topView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.bottom.equalTo(timePickerView.snp.top)
make.leading.trailing.equalToSuperview()
make.height.equalTo(54.scale375Height())
}
topLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
cancelButton.snp.makeConstraints { make in
make.width.height.equalTo(24.scale375())
make.leading.equalToSuperview().offset(20.scale375())
make.centerY.equalToSuperview()
}
sureButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20.scale375())
make.width.height.equalTo(cancelButton)
make.centerY.equalTo(cancelButton)
}
timePickerView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.height.equalTo(270.scale375Height())
make.bottom.equalToSuperview().offset(-5.scale375Height())
}
}
func bindInteraction() {
self.layer.cornerRadius = 12
timePickerView.countDownDuration = timeDuration
timePickerView.addTarget(self, action: #selector(datePickerValueChanged(_:)), for: .valueChanged)
sureButton.addTarget(self, action: #selector(sureAction(sender:)), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelAction(sender:)), for: .touchUpInside)
}
@objc func sureAction(sender: UIButton) {
var conferenceInfo = store.conferenceInfo
conferenceInfo.durationTime = UInt(timePickerView.countDownDuration)
store.update(conference: conferenceInfo)
dismissAction?()
}
@objc func cancelAction(sender: UIButton) {
dismissAction?()
}
@objc func datePickerValueChanged(_ sender: UIDatePicker) {
let selectedTimeInterval = sender.countDownDuration
let minTimeInterval = TimeInterval(900)
if selectedTimeInterval < minTimeInterval {
sender.countDownDuration = minTimeInterval
}
}
deinit{
debugPrint("deinit:\(self)")
}
@Injected(\.modifyScheduleStore) var store: ScheduleConferenceStore
}
private extension String {
static var roomDurationText: String {
localized("Room duration")
}
}

View File

@@ -0,0 +1,154 @@
//
// InviteEnterRoomDataHelper.swift
// TUIRoomKit
//
// Created by janejntang on 2024/7/5.
//
import Foundation
import Factory
class InviteEnterRoomDataHelper {
class func generateInviteEnterRoomHelperData(conferenceInfo: ConferenceInfo, operation: ConferenceStore) -> [ListCellItemData] {
var array: [ListCellItemData] = []
array.append(getRoomNameItem(conferenceInfo: conferenceInfo))
array.append(getRoomTypeItem(conferenceInfo: conferenceInfo))
array.append(getRoomDurationItem(conferenceInfo: conferenceInfo))
array.append(getRoomIdItem(conferenceInfo: conferenceInfo, operation: operation))
if let passwordItem = getRoomPasswordItem(conferenceInfo: conferenceInfo, operation: operation) {
array.append(passwordItem)
}
return array
}
private class func getRoomNameItem(conferenceInfo: ConferenceInfo) -> ListCellItemData {
let roomNameItem = getListCellItem(title: .roomName, message: conferenceInfo.basicInfo.name, hasRightButton: false)
return roomNameItem
}
private class func getRoomTypeItem(conferenceInfo: ConferenceInfo) -> ListCellItemData {
let message: String = conferenceInfo.basicInfo.isSeatEnabled ? .onStageSpeechRoom : .freeSpeechRoom
let roomNameItem = getListCellItem(title: .roomType, message: message, hasRightButton: false)
return roomNameItem
}
private class func getRoomDurationItem(conferenceInfo: ConferenceInfo) -> ListCellItemData {
let scheduleStartTime = getTimeIntervalString(TimeInterval(conferenceInfo.scheduleStartTime), dateFormat: "MM-dd HH:mm")
let scheduleEndTime = getTimeIntervalString(TimeInterval(conferenceInfo.scheduleEndTime), dateFormat: "HH:mm")
let nextDayText = isTimeInNextDay(conferenceInfo: conferenceInfo) ? .nextDay : ""
let message = scheduleStartTime + "-" + nextDayText + scheduleEndTime
let roomNameItem = getListCellItem(title: .roomDuration, message: message, hasRightButton: false)
return roomNameItem
}
private class func isTimeInNextDay(conferenceInfo: ConferenceInfo) -> Bool {
let startDate = Date(timeIntervalSince1970: TimeInterval(conferenceInfo.scheduleStartTime))
let endDate = Date(timeIntervalSince1970: TimeInterval(conferenceInfo.scheduleEndTime))
let calendar = Calendar.current
let startDay = calendar.dateComponents([.year, .month, .day], from: startDate).day ?? 0
let endDay = calendar.dateComponents([.year, .month, .day], from: endDate).day ?? 0
return endDay - startDay == 1
}
private class func getTimeIntervalString(_ time: TimeInterval, dateFormat: String) -> String {
let date = Date(timeIntervalSince1970: time)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = dateFormat
return dateFormatter.string(from: date)
}
private class func getRoomIdItem(conferenceInfo: ConferenceInfo, operation: ConferenceStore) -> ListCellItemData {
let roomIdItem = getListCellItem(title: .roomIdText, message: conferenceInfo.basicInfo.roomId, hasRightButton: true)
roomIdItem.buttonData?.action = { _ in
UIPasteboard.general.string = conferenceInfo.basicInfo.roomId
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .copyRoomIdSuccess)))
}
return roomIdItem
}
private class func getRoomPasswordItem(conferenceInfo: ConferenceInfo, operation: ConferenceStore) -> ListCellItemData? {
let password = conferenceInfo.basicInfo.password
guard password.count > 0 else { return nil }
let passwordItem = getListCellItem(title: .conferencePasswordText, message: password, hasRightButton: true)
passwordItem.buttonData?.action = { _ in
UIPasteboard.general.string = password
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .conferencePasswordSuccess)))
}
return passwordItem
}
private class func getRoomLinkItem(roomId: String, operation: ConferenceStore) -> ListCellItemData? {
guard let roomLink = getRoomLink(roomId: roomId) else { return nil }
let roomLinkItem = getListCellItem(title: .roomLinkText, message: roomLink, hasRightButton: true)
roomLinkItem.buttonData?.action = { _ in
UIPasteboard.general.string = roomLink
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .copyRoomLinkSuccess)))
}
return roomLinkItem
}
private class func getListCellItem(title: String, message: String, hasRightButton: Bool) -> ListCellItemData {
let item = ListCellItemData()
item.titleText = title
item.messageText = message
item.hasRightButton = hasRightButton
if hasRightButton {
item.buttonData = getCopyButtonItem()
}
item.titleColor = UIColor(0x8F9AB2)
item.messageColor = UIColor(0x4F586B)
item.backgroundColor = .clear
return item
}
private class func getCopyButtonItem() -> ButtonItemData {
let buttonData = ButtonItemData()
buttonData.normalIcon = "room_copy"
buttonData.normalTitle = .copyText
buttonData.cornerRadius = 4
buttonData.titleFont = UIFont(name: "PingFangSC-Regular", size: 12)
buttonData.titleColor = UIColor(0x4F586B)
buttonData.backgroundColor = UIColor(0xD5E0F2).withAlphaComponent(0.7)
buttonData.resourceBundle = tuiRoomKitBundle()
return buttonData
}
private class func getRoomLink(roomId: String) -> String? {
guard let bundleId = Bundle.main.bundleIdentifier else { return nil }
if bundleId == "com.tencent.tuiroom.apiexample" || bundleId == "com.tencent.fx.rtmpdemo" {
return "https://web.sdk.qcloud.com/trtc/webrtc/test/tuiroom-inner/index.html#/" + "room?roomId=" + roomId
} else if bundleId == "com.tencent.mrtc" {
return "https://web.sdk.qcloud.com/component/tuiroom/index.html#/" + "room?roomId=" + roomId
} else {
return nil
}
}
@Injected(\.conferenceStore) private var operation
}
private extension String {
static var roomIdText: String {
localized("Room ID")
}
static var roomLinkText: String {
localized("Room link")
}
static var copyText: String {
localized("Copy")
}
static var copyRoomIdSuccess: String {
localized("Conference ID copied.")
}
static var copyRoomLinkSuccess: String {
localized("Conference Link copied.")
}
static let conferencePasswordText = localized("Conference password")
static let conferencePasswordSuccess = localized("Conference password copied successfully.")
static let roomName = localized("Room name")
static let roomType = localized("Room type")
static let roomDuration = localized("Room duration")
static let freeSpeechRoom = localized("Free Speech Room")
static let onStageSpeechRoom = localized("On-stage Speech Room")
static let nextDay = localized("Next Day")
}

View File

@@ -0,0 +1,184 @@
//
// InviteEnterRoomView.swift
// TUIRoomKit
//
// Created by janejntang on 2024/7/5.
//
import Foundation
import Factory
import TUICore
import Combine
enum InviteEnterRoomViewStyle {
case normal
case inviteWhenSuccess
}
class InviteEnterRoomView: UIView {
private var cancellableSet = Set<AnyCancellable>()
var style: InviteEnterRoomViewStyle = .normal
var title: String {
if self.style == .normal {
return .inviteMember
} else {
return .inviteWhenSuccess
}
}
let conferenceInfo: ConferenceInfo
lazy var menus = {
InviteEnterRoomDataHelper.generateInviteEnterRoomHelperData(conferenceInfo: conferenceInfo, operation: operation)
}()
private let dropArrowButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "room_drop_arrow", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
button.contentEdgeInsets = UIEdgeInsets(top: 10.scale375Height(), left: 20.scale375(), bottom: 20.scale375Height(), right: 20.scale375())
return button
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = title
label.textColor = UIColor(0x4F586B)
label.font = UIFont.systemFont(ofSize: 18, weight: .medium)
label.textAlignment = isRTL ? .right : .left
return label
}()
private let stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.alignment = .center
view.distribution = .equalSpacing
view.spacing = 10
view.backgroundColor = .clear
return view
}()
private let copyButton: UIButton = {
let button = UIButton()
button.setTitle(.copyRoomInformation, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
button.setTitleColor(UIColor(0x1C66E5), for: .normal)
button.titleLabel?.textAlignment = .center
button.layer.cornerRadius = 6
button.layer.borderWidth = 1
button.layer.borderColor = UIColor(0x1C66E5).cgColor
return button
}()
init(conferenceInfo: ConferenceInfo, style: InviteEnterRoomViewStyle = .normal) {
self.conferenceInfo = conferenceInfo
self.style = style
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
constructViewHierarchy()
activateConstraints()
bindInteraction()
backgroundColor = UIColor(0xFFFFFF)
layer.cornerRadius = 14
}
private func constructViewHierarchy() {
addSubview(dropArrowButton)
addSubview(titleLabel)
addSubview(stackView)
addSubview(copyButton)
}
private func activateConstraints() {
dropArrowButton.snp.makeConstraints { make in
make.top.equalToSuperview()
make.centerX.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(dropArrowButton.snp.bottom)
make.leading.equalToSuperview().offset(16.scale375())
}
stackView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(20.scale375Height())
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
}
copyButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.height.equalTo(44.scale375Height())
make.top.equalTo(stackView.snp.bottom).offset(20.scale375Height())
make.bottom.equalToSuperview().offset(-34.scale375Height())
}
for item in menus {
let view = ListCellItemView(itemData: item)
stackView.addArrangedSubview(view)
view.snp.makeConstraints { make in
make.height.equalTo(20.scale375Height())
make.width.equalToSuperview()
}
}
}
private func bindInteraction() {
subscribeToast()
dropArrowButton.addTarget(self, action: #selector(dropAction(sender: )), for: .touchUpInside)
copyButton.addTarget(self, action: #selector(copyAction(sender: )), for: .touchUpInside)
}
@objc func dropAction(sender: UIButton) {
route.dismiss(animated: true)
}
@objc func copyAction(sender: UIButton) {
var conferenceDetails = title
menus.forEach { item in
conferenceDetails = conferenceDetails + "\n\(item.titleText) : \(item.messageText)"
}
UIPasteboard.general.string = conferenceDetails
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .roomInformationCopiedSuccessfully)))
}
deinit {
debugPrint("deinit \(self)")
}
@Injected(\.navigation) private var route
@Injected(\.conferenceStore) private var operation
}
extension InviteEnterRoomView {
private func subscribeToast() {
operation.toastSubject
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] toast in
guard let self = self else { return }
var position = TUICSToastPositionBottom
switch toast.position {
case .center:
position = TUICSToastPositionCenter
default:
break
}
self.makeToast(toast.message, duration: toast.duration, position: position)
}
.store(in: &cancellableSet)
}}
private extension String {
static let inviteMember = localized("Invite members to join")
static let inviteWhenSuccess = localized("Booking successful, invite members to join")
static let copyRoomInformation = localized("Copy room information")
static let roomInformationCopiedSuccessfully = localized("Room information copied successfully")
}

View File

@@ -0,0 +1,120 @@
//
// RoomTypeView.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/14.
//
import Foundation
import UIKit
import Factory
class RoomTypeView: UIView {
var dismissAction: (() -> Void)?
let freedomButton: UIButton = {
let button = UIButton()
button.setTitle(.freedomSpeakText, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
button.setTitleColor(UIColor(0x22262E), for: .normal)
button.backgroundColor = UIColor(0xFFFFFF)
return button
}()
let raiseHandButton: UIButton = {
let button = UIButton()
button.setTitle(.raiseHandSpeakText, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
button.setTitleColor(UIColor(0x22262E), for: .normal)
button.backgroundColor = UIColor(0xFFFFFF)
return button
}()
let cancelButton: UIButton = {
let button = UIButton()
button.setTitle(.cancelText, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .regular)
button.setTitleColor(UIColor(0x22262E), for: .normal)
button.backgroundColor = UIColor(0xFFFFFF)
return button
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(freedomButton)
addSubview(raiseHandButton)
addSubview(cancelButton)
}
func activateConstraints() {
freedomButton.snp.makeConstraints { make in
make.top.equalToSuperview()
make.bottom.equalTo(raiseHandButton.snp.top)
make.leading.trailing.equalToSuperview()
make.height.equalTo(58.scale375Height())
}
raiseHandButton.snp.makeConstraints { make in
make.bottom.equalTo(cancelButton.snp.top)
make.leading.trailing.equalToSuperview()
make.height.equalTo(58.scale375Height())
}
cancelButton.snp.makeConstraints { make in
make.bottom.equalToSuperview()
make.leading.trailing.equalToSuperview()
make.height.equalTo(97.scale375Height())
}
}
func bindInteraction() {
layer.cornerRadius = 12
freedomButton.addTarget(self, action: #selector(freedomAction(sender:)), for: .touchUpInside)
raiseHandButton.addTarget(self, action: #selector(raiseHandAction(sender:)), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelAction(sender:)), for: .touchUpInside)
}
@objc func freedomAction(sender: UIButton) {
var conferenceInfo = store.conferenceInfo
conferenceInfo.basicInfo.isSeatEnabled = false
store.update(conference: conferenceInfo)
dismissAction?()
}
@objc func raiseHandAction(sender: UIButton) {
var conferenceInfo = store.conferenceInfo
conferenceInfo.basicInfo.isSeatEnabled = true
store.update(conference: conferenceInfo)
dismissAction?()
}
@objc func cancelAction(sender: UIButton) {
dismissAction?()
}
deinit {
debugPrint("deinit \(self)")
}
@Injected(\.modifyScheduleStore) var store: ScheduleConferenceStore
}
private extension String {
static var raiseHandSpeakText: String {
localized("On-stage Speech Room")
}
static var freedomSpeakText: String {
localized("Free Speech Room")
}
static var cancelText: String {
localized("Cancel")
}
}

View File

@@ -0,0 +1,21 @@
//
// ScheduleBaseCell.swift
// TUIRoomKit
//
// Created by aby on 2024/6/28.
//
import UIKit
import Combine
class ScheduleBaseCell: UITableViewCell {
var cancellableSet = Set<AnyCancellable>()
override func prepareForReuse() {
super.prepareForReuse()
cancellableSet.forEach { cancelable in
cancelable.cancel()
}
cancellableSet.removeAll()
}
}

View File

@@ -0,0 +1,88 @@
//
// CellConfigItem.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/19.
//
import Foundation
import Combine
enum ScheduleConfigCellType {
case list
case switcher
case textField
case button
var cellIdentifier: String {
switch self {
case .list:
return ScheduleTabCell.identifier
case .switcher:
return SwitchCell.identifier
case .textField:
return TextFieldCell.identifier
case .button:
return ButtonCell.identifier
}
}
}
typealias CellSelectClosure = ()->Void
typealias CellStateBinderClosure = (UITableViewCell, inout Set<AnyCancellable>)->Void
protocol CellConfigItem {
var cellType: ScheduleConfigCellType { get }
var title: String { get }
var selectClosure: CellSelectClosure? { get set }
var bindStateClosure: CellStateBinderClosure? { get set }
var isEnable: Bool { get set }
}
struct ListItem: CellConfigItem {
var cellType: ScheduleConfigCellType = .list
var title: String
var content: String = ""
var isEnable: Bool = true
var showButton: Bool = false
var buttonIcon: String = "room_down_arrow1"
var selectClosure: CellSelectClosure?
var bindStateClosure: CellStateBinderClosure?
var iconList: [String] = []
}
struct SwitchItem: CellConfigItem {
var cellType: ScheduleConfigCellType = .switcher
var title: String
var isOn: Bool = true
var isEnable: Bool = true
var selectClosure: CellSelectClosure?
var bindStateClosure: CellStateBinderClosure?
}
struct TextFieldItem: CellConfigItem {
var cellType: ScheduleConfigCellType = .textField
var title: String
var selectClosure: CellSelectClosure?
var bindStateClosure: CellStateBinderClosure?
var saveTextClosure: ((String) -> Void)?
var isEnable: Bool = true
var content: String = ""
var keyboardType: UIKeyboardType = .default
var maxLengthInBytes: Int = 100
var placeholder: String = ""
}
struct ButtonItem: CellConfigItem {
var cellType: ScheduleConfigCellType = .button
var title: String
var selectClosure: CellSelectClosure?
var bindStateClosure: CellStateBinderClosure?
var isEnable: Bool = true
var titleColor: UIColor?
var backgroudColor: UIColor?
}

View File

@@ -0,0 +1,132 @@
//
// ScheduleTabCell.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/5.
//
import Foundation
import Combine
class ScheduleTabCell: ScheduleBaseCell {
static let identifier = "ScheduleTabCell"
let titleLabel: UILabel = {
let view = UILabel()
view.backgroundColor = .clear
view.textColor = UIColor(0x2B2E38)
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textAlignment = isRTL ? .right : .left
return view
}()
let messageLabel: UILabel = {
let view = UILabel()
view.backgroundColor = .clear
view.textColor = UIColor(0x2B2E38).withAlphaComponent(0.7)
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textAlignment = isRTL ? .left : .right
return view
}()
let button: UIButton = {
let button = UIButton()
let image = UIImage(named: "room_down_arrow1", in: tuiRoomKitBundle(), compatibleWith: nil)
button.setImage(image, for: .normal)
return button
}()
let avatarsView: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.alignment = isRTL ? .leading : .trailing
view.spacing = 5
return view
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
isViewReady = true
constructViewHierarchy()
activateConstraints()
setupViewStyle()
}
private func constructViewHierarchy() {
contentView.addSubview(titleLabel)
contentView.addSubview(button)
contentView.addSubview(messageLabel)
contentView.addSubview(avatarsView)
}
private func activateConstraints() {
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20.scale375())
make.width.lessThanOrEqualTo(100.scale375())
make.centerY.equalToSuperview()
}
button.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20.scale375())
make.width.height.equalTo(16.scale375())
make.centerY.equalToSuperview()
}
messageLabel.snp.makeConstraints() { make in
make.trailing.equalTo(button.snp.leading).offset(-5.scale375())
make.centerY.equalToSuperview()
make.width.lessThanOrEqualTo(170.scale375())
}
avatarsView.snp.makeConstraints { make in
make.trailing.equalTo(messageLabel.snp.leading).offset(-5.scale375())
make.centerY.equalToSuperview()
make.width.lessThanOrEqualTo(120.scale375())
}
}
private func setupViewStyle() {
// TODO: - @janejntang use color theme define from design graph.
backgroundColor = UIColor(0xFFFFFF)
}
func updateView(item: CellConfigItem) {
guard let listItem = item as? ListItem else { return }
titleLabel.text = listItem.title
messageLabel.text = listItem.content
button.setImage(UIImage(named: listItem.buttonIcon, in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
updateButton(isShown: listItem.showButton)
updateStackView(iconList: listItem.iconList)
}
func updateStackView(iconList: [String]) {
avatarsView.arrangedSubviews.forEach { view in
view.removeFromSuperview()
}
let placeHolderImage = UIImage(named: "room_default_avatar_rect", in: tuiRoomKitBundle(), compatibleWith: nil)
for iconString in iconList {
let imageView = UIImageView(image: placeHolderImage)
if let url = URL(string: iconString ) {
imageView.sd_setImage(with: url, placeholderImage: placeHolderImage)
}
imageView.snp.makeConstraints { make in
make.width.height.equalTo(32.scale375())
}
avatarsView.addArrangedSubview(imageView)
}
titleLabel.snp.updateConstraints { make in
let maxWidth = iconList.count > 0 ? 80.scale375() : 100.scale375()
make.width.lessThanOrEqualTo(maxWidth)
}
}
func updateButton(isShown: Bool) {
let buttonWidth = isShown ? 16.scale375() : 0
button.snp.updateConstraints { make in
make.width.equalTo(buttonWidth)
}
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,75 @@
//
// SwitchCell.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/13.
//
import Foundation
class SwitchCell: ScheduleBaseCell {
static let identifier = "SwitchCell"
var item: CellConfigItem?
let titleLabel: UILabel = {
let view = UILabel()
view.backgroundColor = .clear
view.textColor = UIColor(0x2B2E38)
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textAlignment = isRTL ? .right : .left
return view
}()
let rightSwitch: UISwitch = {
let view = UISwitch()
view.isOn = true
view.onTintColor = UIColor(0x0062E3)
return view
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
isViewReady = true
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
private func constructViewHierarchy() {
contentView.addSubview(titleLabel)
contentView.addSubview(rightSwitch)
}
private func activateConstraints() {
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20.scale375())
make.centerY.equalToSuperview()
}
rightSwitch.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20.scale375())
make.centerY.equalToSuperview()
}
}
private func bindInteraction() {
rightSwitch.addTarget(self, action: #selector(switchAction(sender:)), for: .touchUpInside)
}
@objc func switchAction(sender: UISwitch) {
item?.selectClosure?()
}
func updateView(item: CellConfigItem) {
guard let switchItem = item as? SwitchItem else { return }
self.item = item
titleLabel.text = switchItem.title
rightSwitch.isOn = switchItem.isOn
rightSwitch.isEnabled = switchItem.isEnable
}
deinit {
debugPrint("deinit:\(self)")
}
}

View File

@@ -0,0 +1,108 @@
//
// TextFieldCell.swift
// TUIRoomKit
//
// Created by janejntang on 2024/7/2.
//
import Foundation
import Factory
class TextFieldCell: ScheduleBaseCell {
static let identifier = "TextFieldCell"
private var item: CellConfigItem?
let titleLabel: UILabel = {
let view = UILabel()
view.backgroundColor = .clear
view.textColor = UIColor(0x2B2E38)
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textAlignment = isRTL ? .right : .left
return view
}()
lazy var textField: UITextField = {
let view = UITextField(frame: .zero)
view.backgroundColor = .clear
view.textColor = UIColor(0x2B2E38).withAlphaComponent(0.7)
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.keyboardType = .default
view.textAlignment = isRTL ? .left : .right
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100.scale375(), height: 50.scale375Height()))
toolbar.barStyle = .default
toolbar.items = [UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil), UIBarButtonItem(title: .ok, style: .done, target: self, action: #selector(saveTextField))]
view.inputAccessoryView = toolbar
view.delegate = self
return view
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
isViewReady = true
constructViewHierarchy()
activateConstraints()
}
private func constructViewHierarchy() {
contentView.addSubview(titleLabel)
contentView.addSubview(textField)
}
private func activateConstraints() {
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20.scale375())
make.centerY.equalToSuperview()
make.width.lessThanOrEqualTo(100.scale375())
}
textField.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-23.scale375())
make.leading.equalTo(titleLabel.snp.trailing).offset(20.scale375())
make.centerY.equalToSuperview()
}
}
func updateView(item: CellConfigItem) {
guard let textFieldItem = item as? TextFieldItem else { return }
self.item = item
titleLabel.text = item.title
textField.text = textFieldItem.content
textField.isEnabled = textFieldItem.isEnable
textField.keyboardType = textFieldItem.keyboardType
textField.placeholder = textFieldItem.placeholder
}
@objc func saveTextField() {
if let textFieldItem = item as? TextFieldItem, let text = textField.text {
textFieldItem.saveTextClosure?(text)
}
textField.resignFirstResponder()
}
deinit {
debugPrint("deinit:\(self)")
}
}
extension TextFieldCell: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let textFieldItem = item as? TextFieldItem else { return false }
guard let text = textField.text else { return true }
if string.count > 0, string.trimmingCharacters(in: .whitespaces).isEmpty, range.location == 0 {
return false
}
let currentLengthInBytes = Array(text.utf8).count
let replacementLengthInBytes = Array(string.utf8).count
if currentLengthInBytes + replacementLengthInBytes > textFieldItem.maxLengthInBytes {
return false
}
return true
}
}
private extension String {
static let ok = localized("OK")
}

View File

@@ -0,0 +1,139 @@
//
// TimePickerView.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/14.
//
import Foundation
import Factory
class TimePickerView: UIView {
var dismissAction: (() -> Void)?
var pickerDate: Date?
let topView: UIView = {
let view = UIView()
return view
}()
let topLabel: UILabel = {
let label = UILabel()
label.text = .startingTimeText
label.font = UIFont.systemFont(ofSize: 16, weight: .heavy)
label.textColor = UIColor(0x22262E)
return label
}()
let cancelButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "schedule_wrong", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
return button
}()
let sureButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "schedule_right", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
return button
}()
lazy var timePickerView: UIDatePicker = {
let pickerView = UIDatePicker()
pickerView.datePickerMode = .dateAndTime
if #available(iOS 14.0, *) {
pickerView.preferredDatePickerStyle = .wheels
}
pickerView.minuteInterval = 5
pickerView.timeZone = store.conferenceInfo.timeZone
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd HH:mm"
var minimumTime = Date().timeIntervalSince1970
let remainder = minimumTime.remainder(dividingBy: 300)
if remainder > 60 || remainder < 0 {
minimumTime = Date().addingTimeInterval(150).timeIntervalSince1970
minimumTime = minimumTime - minimumTime.remainder(dividingBy: 300)
}
if let pickerDate = pickerDate {
pickerView.date = pickerDate
} else {
pickerView.date = Date(timeIntervalSince1970: minimumTime)
}
pickerView.minimumDate = Date(timeIntervalSince1970: minimumTime)
return pickerView
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
backgroundColor = UIColor(0xFFFFFF)
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(topView)
topView.addSubview(topLabel)
topView.addSubview(cancelButton)
topView.addSubview(sureButton)
addSubview(timePickerView)
}
func activateConstraints() {
topView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.bottom.equalTo(timePickerView.snp.top)
make.leading.trailing.equalToSuperview()
make.height.equalTo(54.scale375Height())
}
topLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
cancelButton.snp.makeConstraints { make in
make.width.height.equalTo(24.scale375())
make.leading.equalToSuperview().offset(20.scale375())
make.centerY.equalToSuperview()
}
sureButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20.scale375())
make.width.height.equalTo(cancelButton)
make.centerY.equalTo(cancelButton)
}
timePickerView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.height.equalTo(300.scale375Height())
make.bottom.equalToSuperview().offset(-5.scale375Height())
}
}
func bindInteraction() {
self.layer.cornerRadius = 12
sureButton.addTarget(self, action: #selector(sureAction(sender:)), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelAction(sender:)), for: .touchUpInside)
}
@objc func sureAction(sender: UIButton) {
var conferenceInfo = store.conferenceInfo
conferenceInfo.scheduleStartTime = UInt(timePickerView.date.timeIntervalSince1970)
store.update(conference: conferenceInfo)
dismissAction?()
}
@objc func cancelAction(sender: UIButton) {
dismissAction?()
}
deinit{
debugPrint("deinit:\(self)")
}
@Injected(\.modifyScheduleStore) var store: ScheduleConferenceStore
}
private extension String {
static var startingTimeText: String {
localized("Starting time")
}
}

View File

@@ -0,0 +1,197 @@
//
// TimeZoneView.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/27.
//
import Foundation
import Factory
class TimeZoneView: UIView {
lazy var menus = TimeZone.knownTimeZoneIdentifiers.sorted { id1, id2 in
let timeZone1 = TimeZone(identifier: id1)?.secondsFromGMT() ?? 0
let timeZone2 = TimeZone(identifier: id2)?.secondsFromGMT() ?? 0
return timeZone1 < timeZone2
}
var selectedTimeZone: String? {
didSet {
self.updateSelected()
}
}
let backButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "room_back_black", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
button.contentEdgeInsets = UIEdgeInsets(top: 10.scale375Height(), left: 20.scale375(), bottom: 10.scale375Height(), right: 20.scale375())
return button
}()
let topLabel: UILabel = {
let label = UILabel()
label.text = .selectTimeZoneText
label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(0x2B2E38)
label.textAlignment = .center
return label
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = UIColor(0x17181F)
tableView.register(TimeZoneCell.self, forCellReuseIdentifier: TimeZoneCell.identifier)
tableView.backgroundColor = UIColor(0xFFFFFF)
return tableView
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
backgroundColor = UIColor(0xFFFFFF)
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
selectedTimeZone = self.store.conferenceInfo.timeZone.identifier
}
func constructViewHierarchy() {
addSubview(backButton)
addSubview(topLabel)
addSubview(tableView)
}
func activateConstraints() {
backButton.snp.makeConstraints { make in
make.top.equalTo(safeAreaLayoutGuide.snp.top)
make.leading.equalToSuperview()
}
topLabel.snp.makeConstraints { make in
make.centerY.equalTo(backButton)
make.centerX.equalToSuperview()
}
tableView.snp.makeConstraints { make in
make.top.equalTo(backButton.snp.bottom).offset(10.scale375Height())
make.leading.equalToSuperview().offset(20.scale375())
make.trailing.equalToSuperview().offset(-20.scale375())
make.bottom.equalToSuperview().offset(-20.scale375Height())
}
}
func bindInteraction() {
backButton.addTarget(self, action: #selector(backAction(sender: )), for: .touchUpInside)
}
@objc func backAction(sender: UIButton) {
route.pop()
}
private func updateSelected() {
if let selectedTimeZone = selectedTimeZone,
let index = menus.firstIndex(of: selectedTimeZone) {
let indexPath = IndexPath(row: index, section: 0)
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .top)
}
}
deinit{
debugPrint("deinit:\(self)")
}
@Injected(\.navigation) private var route
@Injected(\.modifyScheduleStore) var store: ScheduleConferenceStore
}
extension TimeZoneView: UITableViewDataSource {
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menus.count
}
}
extension TimeZoneView: UITableViewDelegate {
internal func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TimeZoneCell.identifier, for: indexPath)
if let timeZoneCell = cell as? TimeZoneCell, let timeZone = TimeZone(identifier: menus[indexPath.row]) {
timeZoneCell.title = timeZone.getTimeZoneName()
if selectedTimeZone == menus[indexPath.row] {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
return cell
}
internal func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let timeZone = TimeZone(identifier: menus[indexPath.row]) {
var conferenceInfo = store.conferenceInfo
conferenceInfo.timeZone = timeZone
conferenceInfo.scheduleStartTime = UInt(conferenceInfo.scheduleStartTime)
store.update(conference: conferenceInfo)
}
route.pop()
}
internal func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50.scale375Height()
}
}
private extension String {
static var roomDurationText: String {
localized("Room duration")
}
static let selectTimeZoneText: String = localized("Select time zone")
}
class TimeZoneCell: UITableViewCell {
static let identifier = "TimeZoneCell"
var title: String = "" {
didSet {
label.text = title
}
}
let label: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 16, weight: .regular)
label.textColor = UIColor(0x22262E)
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: TimeZoneCell.identifier)
self.selectionStyle = .none
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
backgroundColor = UIColor(0xFFFFFF)
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(label)
}
func activateConstraints() {
label.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}

View File

@@ -0,0 +1,27 @@
//
// TimeZoneViewController.swift
// TUIRoomKit
//
// Created by janejntang on 2024/6/27.
//
import Foundation
class TimeZoneViewController: UIViewController {
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override func loadView() {
self.view = TimeZoneView()
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,20 @@
//
// AudioModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/3/8.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
class AudioModel {
var isMicOpened: Bool = false
var isSoundOnSpeaker: Bool = true
var captureVolume: Int = 100
var playVolume: Int = 100
var volumePrompt: Bool = true
var isRecord: Bool = false
var audioQuality: TUIAudioQuality = .default
}

View File

@@ -0,0 +1,41 @@
//
// ConferenceListManagerObserver.swift
// TUIRoomKit
//
// Created by janejntang on 2024/7/23.
//
import Foundation
import RTCRoomEngine
class ConferenceListObserver: NSObject, TUIConferenceListManagerObserver {
private var roomInfo: TUIRoomInfo {
EngineManager.shared.store.roomInfo
}
func onConferenceInfoChanged(conferenceInfo: TUIConferenceInfo, modifyFlag: TUIConferenceModifyFlag) {
guard conferenceInfo.basicRoomInfo.roomId == roomInfo.roomId else { return }
roomInfo.name = conferenceInfo.basicRoomInfo.name
EngineEventCenter.shared.notifyEngineEvent(event: .onConferenceInfoChanged, param: ["conferenceInfo": conferenceInfo, "modifyFlag": modifyFlag])
}
func onConferenceScheduled(conferenceInfo: TUIConferenceInfo) {
}
func onConferenceWillStart(conferenceInfo: TUIConferenceInfo) {
}
func onConferenceCancelled(roomId: String, reason: TUIConferenceCancelReason, operateUser: TUIUserInfo) {
}
func onScheduleAttendeesChanged(roomId: String, leftUsers: [TUIUserInfo], joinedUsers: [TUIUserInfo]) {
}
func onConferenceStatusChanged(roomId: String, status: TUIConferenceStatus) {
}
}

View File

@@ -0,0 +1,24 @@
//
// ConferenceParams.swift
// TUIRoomKit
//
// Created by janejntang on 2024/3/14.
//
import Foundation
@objcMembers public class ConferenceParams : NSObject {
public var isMuteMicrophone = false
public var isOpenCamera = false
public var isSoundOnSpeaker = true
public var name: String?
public var enableMicrophoneForAllUser = true
public var enableCameraForAllUser = true
public var enableMessageForAllUser = true
public var enableSeatControl = false
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,127 @@
//
// ConferenceSessionImp.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/8/13.
//
import Foundation
import RTCRoomEngine
import Factory
class ConferenceSessionImp: NSObject {
private(set) var isEnableWaterMark = false;
private(set) var waterMarkText = "";
private var observers = NSHashTable<ConferenceObserver>.weakObjects()
// MARK: - Public
override init() {
super.init()
subscribeEngine()
}
func addObserver(observer: ConferenceObserver) {
guard !observers.contains(observer) else { return }
observers.add(observer)
}
func removeObserver(observer: ConferenceObserver) {
guard observers.contains(observer) else { return }
observers.remove(observer)
}
func destroy() {
unsubscribeEngine()
observers.removeAllObjects()
}
func enableWaterMark() {
self.isEnableWaterMark = true
}
func setWaterMarkText(waterMarkText: String) {
self.waterMarkText = waterMarkText
}
func setContactsViewProvider(_ provider: @escaping (ConferenceParticipants) -> ContactViewProtocol) {
Container.shared.contactViewController.register { participants in
provider(participants)
}
}
deinit {
unsubscribeEngine()
}
// MARK: - Private
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onExitedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onDestroyedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onStartedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onJoinedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRoomDismissed, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOutOfRoom, observer: self)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onExitedRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onDestroyedRoom, observer: self)
}
}
// MARK: - callback
extension ConferenceSessionImp: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onStartedRoom:
guard let roomInfo = param?["roomInfo"] as? TUIRoomInfo else { return }
guard let error = param?["error"] as? TUIError else { return }
guard let message = param?["mesasge"] as? String else { return }
handleRoomStarted(roomInfo: roomInfo, error: error, message: message)
case .onJoinedRoom:
guard let roomInfo = param?["roomInfo"] as? TUIRoomInfo else { return }
guard let error = param?["error"] as? TUIError else { return }
guard let message = param?["mesasge"] as? String else { return }
handleRoomJoined(roomInfo: roomInfo, error: error, message: message)
case .onDestroyedRoom, .onRoomDismissed:
guard let roomId = param?["roomId"] as? String else { return }
handleRoomFinished(roomId: roomId)
case .onExitedRoom, .onKickedOutOfRoom:
guard let roomId = param?["roomId"] as? String else { return }
handleRoomExited(roomId: roomId)
default: break
}
}
private func handleRoomStarted(roomInfo: TUIRoomInfo, error: TUIError, message: String) {
for observer in observers.allObjects {
observer.onConferenceStarted?(roomInfo: roomInfo, error: error, message: message)
}
}
private func handleRoomJoined(roomInfo: TUIRoomInfo, error: TUIError, message: String) {
for observer in observers.allObjects {
observer.onConferenceJoined?(roomInfo: roomInfo, error: error, message: message)
}
}
private func handleRoomFinished(roomId: String) {
for observer in observers.allObjects {
observer.onConferenceFinished?(roomId: roomId)
}
}
private func handleRoomExited(roomId: String) {
for observer in observers.allObjects {
observer.onConferenceExited?(roomId: roomId)
}
}
}
extension Container {
var contactViewController: ParameterFactory<ConferenceParticipants, ContactViewProtocol?> {
promised().scope(.unique)
}
}

View File

@@ -0,0 +1,183 @@
//
// EngineEventCenter.swift
// TUIRoomKit
//
// Created by aby on 2023/1/8.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import TUICore
protocol RoomKitUIEventResponder: NSObject {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable: Any]?)
}
protocol RoomEngineEventResponder: NSObject {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String: Any]?)
}
class TUINotificationAdapter:NSObject ,TUINotificationProtocol {
weak var responder: RoomKitUIEventResponder?
init(responder: RoomKitUIEventResponder? = nil) {
self.responder = responder
}
func onNotifyEvent(_ key: String, subKey: String, object anObject: Any?, param: [AnyHashable : Any]?) {
guard let eventKey = EngineEventCenter.RoomUIEvent(rawValue: subKey) else { return }
responder?.onNotifyUIEvent(key: eventKey, Object: anObject, info: param)
}
deinit {
debugPrint("deinit \(self)")
}
}
class EngineEventCenter: NSObject {
// Weak Ref
typealias WeakArray<T> = [() -> T?]
static let shared = EngineEventCenter()
private var engineObserverMap: [RoomEngineEvent: WeakArray<RoomEngineEventResponder>] = [:]
private var uiEventObserverMap: [RoomUIEvent: [TUINotificationAdapter]] = [:]
private override init() {
super.init()
}
enum RoomEngineEvent: String {
case onKickedOffLine
case onRoomDismissed
case onKickedOutOfRoom
case onUserVideoStateChanged
case onUserAudioStateChanged
case onUserVoiceVolumeChanged
case onUserScreenCaptureStopped
case onRequestReceived
case onSendMessageForUserDisableChanged
case onRemoteUserEnterRoom
case onRemoteUserLeaveRoom
case onUserRoleChanged
case onSeatListChanged
case onAllUserCameraDisableChanged
case onAllUserMicrophoneDisableChanged
case onKickedOffSeat
case onStatistics
case onDeletedTakeSeatRequest
case onExitedRoom
case onDestroyedRoom
case onStartedRoom
case onJoinedRoom
case onConferenceInfoChanged
case onInitialSelfUserInfo
case onInitialRoomInfo
case onGetUserListFinished
}
enum RoomUIEvent: String {
case TUIRoomKitService
case TUIRoomKitService_RenewUserList
case TUIRoomKitService_SomeoneSharing
case TUIRoomKitService_RenewSeatList
case TUIRoomKitService_UserOnSeatChanged
case TUIRoomKitService_ShowRoomMainView
case TUIRoomKitService_ShowRoomVideoFloatView
case TUIRoomKitService_CurrentUserHasAudioStream
case TUIRoomKitService_CurrentUserHasVideoStream
case TUIRoomKitService_CurrentUserRoleChanged
case TUIRoomKitService_CurrentUserMuteMessage
case TUIRoomKitService_RoomOwnerChanged
case TUIRoomKitService_ChangeToolBarHiddenState
case TUIRoomKitService_SetToolBarDelayHidden
case TUIRoomKitService_HiddenChatWindow
case TUIRoomKitService_ShowExitRoomView
case TUIRoomKitService_RenewVideoSeatView
case TUIRoomKitService_DismissConferenceViewController
case TUIRoomKitService_ShowFloatChatView
}
func subscribeUIEvent(key: RoomUIEvent, responder: RoomKitUIEventResponder) {
let observer = TUINotificationAdapter(responder: responder)
if var observerArray = uiEventObserverMap[key] {
observerArray.append(observer)
uiEventObserverMap[key] = observerArray
} else {
uiEventObserverMap[key] = [observer]
}
DispatchQueue.main.async {
TUICore.registerEvent(RoomUIEvent.TUIRoomKitService.rawValue, subKey: key.rawValue, object: observer)
}
}
func unsubscribeUIEvent(key: RoomUIEvent, responder: RoomKitUIEventResponder) {
guard var observerArray = uiEventObserverMap[key] else { return }
observerArray = observerArray.filter({ observer in
guard let responderValue = observer.responder else {
DispatchQueue.main.async {
TUICore.unRegisterEvent(RoomUIEvent.TUIRoomKitService.rawValue, subKey: key.rawValue, object: observer)
}
return false
}
if responderValue.isEqual(responder) {
DispatchQueue.main.async {
TUICore.unRegisterEvent(RoomUIEvent.TUIRoomKitService.rawValue, subKey: key.rawValue, object: observer)
}
return false
} else {
return true
}
})
if observerArray.count == 0 {
uiEventObserverMap.removeValue(forKey: key)
} else {
uiEventObserverMap[key] = observerArray
}
}
func notifyUIEvent(key: RoomUIEvent, param: [AnyHashable : Any]) {
DispatchQueue.main.async {
TUICore.notifyEvent(RoomUIEvent.TUIRoomKitService.rawValue, subKey: key.rawValue, object: nil, param: param)
}
}
func subscribeEngine(event: RoomEngineEvent, observer: RoomEngineEventResponder) {
let weakObserver = { [weak observer] in return observer }
if var observerArray = engineObserverMap[event] {
let listenerObject = observerArray.first { weakObject in
guard let object = weakObject() else { return false }
return object.isEqual(observer)
}
guard listenerObject == nil else { return }
observerArray.append(weakObserver)
engineObserverMap[event] = observerArray
} else {
engineObserverMap[event] = [weakObserver]
}
}
func unsubscribeEngine(event: RoomEngineEvent, observer: RoomEngineEventResponder) {
guard var observerArray = engineObserverMap[event] else { return }
observerArray.removeAll { weakObject in
guard let object = weakObject() else { return true }
return object.isEqual(observer)
}
if observerArray.count == 0 {
engineObserverMap.removeValue(forKey: event)
} else {
engineObserverMap[event] = observerArray
}
}
func notifyEngineEvent(event: RoomEngineEvent, param: [String : Any]) {
guard let observers = engineObserverMap[event] else { return }
observers.forEach { responder in
responder()?.onEngineEvent(name: event, param: param)
}
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,818 @@
//
// EngineManager.swift
// TUIRoomKit
//
// Created by WesleyLei on 2022/9/22.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import TUICore
import RTCRoomEngine
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
class EngineManager: NSObject {
static private(set) var shared = EngineManager()
private(set) lazy var store: RoomStore = {
let store = RoomStore()
return store
}()
private(set) var roomEngine = TUIRoomEngine.sharedInstance()
private lazy var eventDispatcher: RoomEventDispatcher = {
let eventDispatcher = RoomEventDispatcher()
return eventDispatcher
}()
private lazy var observer: TRTCObserver = {
let observer = TRTCObserver()
return observer
}()
private lazy var conferenceListObserver: ConferenceListObserver = {
return ConferenceListObserver()
}()
private lazy var conferenceListManager: TUIConferenceListManager? = {
guard let listManager = roomEngine.getExtension(extensionType: .conferenceListManager) as? TUIConferenceListManager else { return nil }
return listManager
}()
private let takeSeatTimeOutNumber: Double = 60
private let openRemoteDeviceTimeOutNumber: Double = 15
private let rootRouter: RoomRouter = RoomRouter.shared
private var isLoginEngine: Bool = false
private let appGroupString: String = "com.tencent.TUIRoomTXReplayKit-Screen"
override private init() {
super.init()
}
deinit {
debugPrint("deinit \(self)")
}
func setSelfInfo(userName: String, avatarURL: String, onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
store.currentUser.userName = userName
store.currentUser.avatarUrl = avatarURL
TUIRoomEngine.setSelfInfo(userName: userName, avatarUrl: avatarURL) {
onSuccess()
} onError: { code, message in
onError(code, message)
debugPrint("---setSelfInfo,code:\(code),message:\(message)")
}
}
func createRoom(roomInfo: TUIRoomInfo, onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
roomInfo.name = transferConferenceName(conferenceName: roomInfo.name)
if !isLoginEngine {
self.login(sdkAppId: Int(TUILogin.getSdkAppID()), userId: TUILogin.getUserID() ?? "", userSig: TUILogin.getUserSig() ?? "")
{ [weak self] in
guard let self = self else { return }
self.isLoginEngine = true
self.createEngineRoom(roomInfo: roomInfo, onSuccess: onSuccess, onError: onError)
} onError: { code, message in
onError(code, message)
}
} else {
createEngineRoom(roomInfo: roomInfo, onSuccess: onSuccess, onError: onError)
}
}
func enterRoom(roomId: String, options: TUIEnterRoomOptions? = nil, enableAudio: Bool, enableVideo: Bool, isSoundOnSpeaker: Bool,
onSuccess: @escaping TUIRoomInfoBlock, onError: @escaping TUIErrorBlock) {
store.videoSetting.isCameraOpened = enableVideo
store.audioSetting.isSoundOnSpeaker = isSoundOnSpeaker
setFramework()
if !isLoginEngine {
self.login(sdkAppId: Int(TUILogin.getSdkAppID()), userId: TUILogin.getUserID() ?? "", userSig: TUILogin.getUserSig() ?? "")
{ [weak self] in
guard let self = self else { return }
self.isLoginEngine = true
self.enterEngineRoom(roomId: roomId, options: options, enableAudio: enableAudio, onSuccess: onSuccess, onError: onError)
} onError: { code, message in
onError(code, message)
}
} else {
enterEngineRoom(roomId: roomId, options: options, enableAudio: enableAudio, onSuccess: onSuccess, onError: onError)
}
}
func exitRoom(onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.exitRoom(syncWaiting: false) { [weak self] in
guard let self = self else { return }
self.handleExitRoomResult()
onSuccess?()
} onError: { [weak self] code, message in
guard let self = self else { return }
self.handleExitRoomResult()
onError?(code, message)
}
}
func destroyRoom(onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.destroyRoom { [weak self] in
guard let self = self else { return }
self.handleDestroyRoomResult()
onSuccess?()
} onError: { [weak self] code, message in
guard let self = self else { return }
self.handleDestroyRoomResult()
onError?(code, message)
}
}
func destroyEngineManager() {
removeEngineObserver()
unsubLogoutNotification()
store = RoomStore()
}
func muteLocalAudio() {
roomEngine.muteLocalAudio()
}
func unmuteLocalAudio(onSuccess:TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.unmuteLocalAudio {
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
}
func openLocalMicrophone(onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
let actionBlock = { [weak self] in
guard let self = self else { return }
self.roomEngine.openLocalMicrophone(self.store.audioSetting.audioQuality) { [weak self] in
guard let self = self else { return }
self.store.audioSetting.isMicOpened = true
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
}
if RoomCommon.checkAuthorMicStatusIsDenied() {
actionBlock()
} else {
RoomCommon.micStateActionWithPopCompletion { granted in
if granted {
actionBlock()
}
}
}
}
func closeLocalCamera() {
store.videoSetting.isCameraOpened = false
roomEngine.closeLocalCamera()
}
func openLocalCamera(onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
let actionBlock = { [weak self] in
guard let self = self else { return }
self.store.videoSetting.isCameraOpened = true
self.roomEngine.openLocalCamera(isFront: self.store.videoSetting.isFrontCamera, quality:
self.store.videoSetting.videoQuality) {
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
}
if RoomCommon.checkAuthorCamaraStatusIsDenied() {
actionBlock()
} else {
RoomCommon.cameraStateActionWithPopCompletion { granted in
if granted {
actionBlock()
}
}
}
}
func switchCamera() {
store.videoSetting.isFrontCamera = !store.videoSetting.isFrontCamera
roomEngine.getMediaDeviceManager().switchCamera(store.videoSetting.isFrontCamera)
}
func switchMirror() {
store.videoSetting.isMirror = !store.videoSetting.isMirror
let params = TRTCRenderParams()
params.mirrorType = store.videoSetting.isMirror ? .enable : .disable
setLocalRenderParams(params: params)
}
func muteAllAudioAction(isMute: Bool, onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
store.roomInfo.isMicrophoneDisableForAllUser = isMute
roomEngine.disableDeviceForAllUserByAdmin(device: .microphone, isDisable:
store.roomInfo.isMicrophoneDisableForAllUser) {
onSuccess()
} onError: { code, message in
onError(code, message)
}
}
func muteAllVideoAction(isMute: Bool, onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
store.roomInfo.isCameraDisableForAllUser = isMute
roomEngine.disableDeviceForAllUserByAdmin(device: .camera, isDisable:
store.roomInfo.isCameraDisableForAllUser) {
onSuccess()
} onError: { code, message in
onError(code, message)
}
}
func takeUserOnSeatByAdmin(userId: String, timeout: Double,
onAccepted: @escaping TUIRequestAcceptedBlock,
onRejected: @escaping TUIRequestRejectedBlock,
onCancelled: @escaping TUIRequestCancelledBlock,
onTimeout: @escaping TUIRequestTimeoutBlock,
onError: @escaping TUIRequestErrorBlock) {
roomEngine.takeUserOnSeatByAdmin(-1, userId: userId, timeout: timeout) { requestId, userId in
onAccepted(requestId, userId)
} onRejected: { requestId, userId, message in
onRejected( requestId, userId, message)
} onCancelled: { requestId, userId in
onCancelled(requestId, userId)
} onTimeout: { requestId, userId in
onTimeout(requestId, userId)
} onError: { requestId, userId, code, message in
onError(requestId, userId, code, message)
}
}
func setAudioRoute(isSoundOnSpeaker: Bool) {
store.audioSetting.isSoundOnSpeaker = isSoundOnSpeaker
let route: TUIAudioRoute = isSoundOnSpeaker ? .speakerphone : .earpiece
roomEngine.getMediaDeviceManager().setAudioRoute(route)
}
func takeSeat(onAccepted: TUIRequestAcceptedBlock? = nil,
onRejected: TUIRequestRejectedBlock? = nil,
onCancelled: TUIRequestCancelledBlock? = nil,
onTimeout: TUIRequestTimeoutBlock? = nil,
onError: TUIRequestErrorBlock? = nil) -> TUIRequest {
let request = self.roomEngine.takeSeat(-1, timeout: takeSeatTimeOutNumber) { [weak self] requestId, userId in
guard let self = self else { return }
self.store.currentUser.isOnSeat = true
self.store.selfTakeSeatRequestId = nil
onAccepted?(requestId, userId)
} onRejected: { [weak self] requestId, userId, message in
guard let self = self else { return }
self.store.selfTakeSeatRequestId = nil
onRejected?(requestId, userId, message)
} onCancelled: { [weak self] requestId, userId in
guard let self = self else { return }
self.store.selfTakeSeatRequestId = nil
onCancelled?(requestId, userId)
} onTimeout: { [weak self] requestId, userId in
guard let self = self else { return }
self.store.selfTakeSeatRequestId = nil
onTimeout?(requestId, userId)
} onError: { [weak self] requestId, userId, code, message in
guard let self = self else { return }
self.store.selfTakeSeatRequestId = nil
onError?(requestId, userId, code, message)
}
store.selfTakeSeatRequestId = request.requestId
return request
}
func cancelTakeSeatRequest() {
guard let requestId = store.selfTakeSeatRequestId else { return }
cancelRequest(requestId)
store.selfTakeSeatRequestId = nil
}
func fetchRoomInfo(roomId: String ,onSuccess: TUIRoomInfoBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.fetchRoomInfo(roomId: roomId, roomType: .conference) { [weak self] roomInfo in
guard let self = self, let roomInfo = roomInfo else { return }
if roomId == self.store.roomInfo.roomId {
self.store.initialRoomInfo(roomInfo)
}
onSuccess?(roomInfo)
} onError: { code, message in
onError?(code, message)
debugPrint("fetchRoomInfo,code:\(code), message:\(message)")
}
}
func setLocalRenderParams(params: TRTCRenderParams) {
roomEngine.getTRTCCloud().setLocalRenderParams(params)
}
func setGSensorMode(mode: TRTCGSensorMode) {
roomEngine.getTRTCCloud().setGSensorMode(mode)
}
func setLocalVideoView(streamType: TUIVideoStreamType, view: UIView?) {
roomEngine.setLocalVideoView(streamType: streamType, view: view)
}
func changeUserRole(userId: String, role: TUIRole, onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
roomEngine.changeUserRole(userId: userId, role: role, onSuccess: onSuccess, onError: onError)
}
func responseRemoteRequest(_ requestId: String, agree: Bool, onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.responseRemoteRequest(requestId, agree: agree) {
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
}
func getUserInfo(_ userId: String, onSuccess: @escaping TUIUserInfoBlock, onError: @escaping TUIErrorBlock) {
roomEngine.getUserInfo(userId, onSuccess: onSuccess, onError: onError)
}
func stopScreenCapture() {
roomEngine.stopScreenCapture()
}
func setVideoEncoder(videoQuality: TUIVideoQuality? = nil, bitrate: Int? = nil, fps: Int? = nil) {
let param = TUIRoomVideoEncoderParams()
store.videoSetting.videoQuality = videoQuality ?? store.videoSetting.videoQuality
param.videoResolution = store.videoSetting.videoQuality
store.videoSetting.videoBitrate = bitrate ?? store.videoSetting.videoBitrate
param.bitrate = store.videoSetting.videoBitrate
store.videoSetting.videoFps = fps ?? store.videoSetting.videoFps
param.fps = store.videoSetting.videoFps
roomEngine.updateVideoQualityEx(streamType: .cameraStream, params: param)
}
func cancelRequest(_ requestId: String, onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.cancelRequest(requestId) {
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
}
func leaveSeat(onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.leaveSeat {
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
}
func startScreenCapture() {
roomEngine.startScreenCapture(appGroup: appGroupString)
}
func stopPlayRemoteVideo(userId: String, streamType: TUIVideoStreamType) {
roomEngine.stopPlayRemoteVideo(userId: userId, streamType: streamType)
}
func setRemoteVideoView(userId: String, streamType: TUIVideoStreamType, view: UIView?) {
roomEngine.setRemoteVideoView(userId: userId, streamType: streamType, view: view)
}
func startPlayRemoteVideo(userId: String, streamType: TUIVideoStreamType, onSuccess: TUISuccessBlock? = nil,
onLoading: TUIPlayOnLoadingBlock? = nil, onError: TUIPlayOnErrorBlock? = nil) {
roomEngine.startPlayRemoteVideo(userId: userId, streamType: streamType, onPlaying: { _ in
guard let onSuccess = onSuccess else { return }
onSuccess()
}, onLoading: { userId in
guard let onLoading = onLoading else { return }
onLoading(userId)
}, onError: { userId, code, message in
guard let onError = onError else { return }
onError(userId, code, message)
})
}
func setAudioCaptureVolume(_ captureVolume: Int) {
store.audioSetting.captureVolume = captureVolume
roomEngine.getTRTCCloud().setAudioCaptureVolume(captureVolume)
}
func setAudioPlayoutVolume(_ playVolume: Int) {
store.audioSetting.playVolume = playVolume
roomEngine.getTRTCCloud().setAudioPlayoutVolume(playVolume)
}
func enableAudioVolumeEvaluation(isVolumePrompt: Bool) {
store.audioSetting.volumePrompt = isVolumePrompt
if isVolumePrompt {
roomEngine.getTRTCCloud().enableAudioVolumeEvaluation(300, enable_vad: true)
} else {
roomEngine.getTRTCCloud().enableAudioVolumeEvaluation(0, enable_vad: false)
}
}
func closeRemoteDeviceByAdmin(userId: String, device: TUIMediaDevice, onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
roomEngine.closeRemoteDeviceByAdmin(userId: userId, device: device, onSuccess: onSuccess, onError: onError)
}
func openRemoteDeviceByAdmin(userId: String, device: TUIMediaDevice,
onAccepted: TUIRequestAcceptedBlock? = nil,
onRejected: TUIRequestRejectedBlock? = nil,
onCancelled: TUIRequestCancelledBlock? = nil,
onTimeout: TUIRequestTimeoutBlock? = nil,
onError: TUIRequestErrorBlock? = nil) {
roomEngine.openRemoteDeviceByAdmin(userId: userId, device: device, timeout: openRemoteDeviceTimeOutNumber, onAccepted: { requestId, userId in
onAccepted?(requestId, userId)
}, onRejected: { requestId, userId, message in
onRejected?(requestId, userId, message)
}, onCancelled: { requestId, userId in
onCancelled?(requestId, userId)
}, onTimeout: { requestId, userId in
onTimeout?(requestId, userId)
}) { requestId, userId, code, message in
onError?(requestId, userId, code, message)
}
}
func disableSendingMessageByAdmin(userId: String, isDisable: Bool, onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.disableSendingMessageByAdmin(userId: userId, isDisable: isDisable) {
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
}
func kickUserOffSeatByAdmin(userId: String, onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.kickUserOffSeatByAdmin(-1, userId: userId) {
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
}
func kickRemoteUserOutOfRoom(userId: String, onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
roomEngine.kickRemoteUserOutOfRoom(userId) {
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
}
func initUserList(onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
self.getUserList(nextSequence: 0, localUserList: []) { [weak self] in
guard let self = self else { return }
if self.store.roomInfo.isSeatEnabled {
self.getSeatList {
onSuccess?()
} onError: { code, message in
onError?(code, message)
}
} else {
onSuccess?()
}
} onError: { code, message in
onError?(code, message)
}
}
func updateVideoQuality(quality: TUIVideoQuality) {
roomEngine.updateVideoQuality(quality)
}
func enableGravitySensor(enable: Bool) {
roomEngine.enableGravitySensor(enable: enable)
}
func setVideoResolutionMode(streamType: TUIVideoStreamType, resolutionMode: TUIResolutionMode) {
roomEngine.setVideoResolutionMode(streamType: streamType, resolutionMode: resolutionMode)
}
func changeRaiseHandNoticeState(isShown: Bool) {
store.isShownRaiseHandNotice = isShown
}
func setRemoteRenderParams(userId: String, streamType: TRTCVideoStreamType, params: TRTCRenderParams) {
roomEngine.getTRTCCloud().setRemoteRenderParams(userId, streamType: streamType, params: params)
}
func updateSeatApplicationList() {
roomEngine.getSeatApplicationList { [weak self] list in
guard let self = self else { return }
self.store.setInviteSeatList(list: list)
} onError: { code, message in
debugPrint("getSeatApplicationList,code:\(code),message:\(message)")
}
}
}
// MARK: - Private
extension EngineManager {
private func login(sdkAppId: Int, userId: String, userSig: String, onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
V2TIMManager.sharedInstance().initSDK(Int32(sdkAppId), config: V2TIMSDKConfig())
store.currentUser.userId = userId
TUIRoomEngine.login(sdkAppId: sdkAppId, userId: userId, userSig: userSig) { [weak self] in
guard let self = self else { return }
self.isLoginEngine = true
onSuccess()
} onError: { code, message in
onError(code, message)
}
}
private func createEngineRoom(roomInfo: TUIRoomInfo, onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
guard !store.isEnteredRoom else {
if store.roomInfo.roomId == roomInfo.roomId {
RoomVideoFloatView.dismiss()
onSuccess()
} else {
onError(.failed, .inAnotherRoomText)
}
return
}
self.store.roomInfo = roomInfo
self.roomEngine.createRoom(roomInfo) {
onSuccess()
} onError: { [weak self] code, message in
guard let self = self else { return }
self.destroyEngineManager()
onError(code, message)
}
}
private func transferConferenceName(conferenceName: String?) -> String {
if let confName = conferenceName, !confName.isEmpty {
return confName
}
let selfInfo = TUIRoomEngine.getSelfInfo()
let name: String = selfInfo.userName.isEmpty ? selfInfo.userId : selfInfo.userName
return name + .quickConferenceText
}
private func enterEngineRoom(roomId: String, options: TUIEnterRoomOptions? = nil, enableAudio: Bool, onSuccess: @escaping TUIRoomInfoBlock, onError: @escaping TUIErrorBlock) {
guard !store.isEnteredRoom else {
if store.roomInfo.roomId == roomId {
onSuccess(store.roomInfo)
} else {
onError(.failed, .inAnotherRoomText)
}
return
}
if let options = options {
roomEngine.enterRoom(roomId, roomType: .conference, options: options) { [weak self] roomInfo in
guard let self = self else { return }
guard let roomInfo = roomInfo else { return }
self.handleEnterRoomResult(roomInfo: roomInfo, enableAudio: enableAudio, onSuccess: onSuccess, onError: onError)
} onError: { [weak self] code, message in
guard let self = self else { return }
self.destroyEngineManager()
onError(code, message)
}
} else {
roomEngine.enterRoom(roomId) { [weak self] roomInfo in
guard let self = self else { return }
guard let roomInfo = roomInfo else { return }
self.handleEnterRoomResult(roomInfo: roomInfo, enableAudio: enableAudio, onSuccess: onSuccess, onError: onError)
} onError: { [weak self] code, message in
guard let self = self else { return }
self.destroyEngineManager()
onError(code, message)
}
}
}
private func handleEnterRoomResult(roomInfo: TUIRoomInfo, enableAudio: Bool, onSuccess: @escaping TUIRoomInfoBlock, onError: @escaping TUIErrorBlock) {
//Update the room entry data stored
addEngineObserver()
store.initialRoomInfo(roomInfo)
store.initialRoomCurrentUser()
//Initialize user list
initUserList()
//Initialize video settings
initLocalVideoState()
subLogoutNotification()
updateSeatApplicationList()
if !isNeededAutoTakeSeat() {
operateLocalMicrophone(enableAudio: enableAudio)
updateCameraState()
store.initalEnterRoomMessage()
onSuccess(roomInfo)
} else {
autoTakeSeatForOwner { [weak self] in
guard let self = self else { return }
self.operateLocalMicrophone(enableAudio: enableAudio)
self.updateCameraState()
self.store.initalEnterRoomMessage()
onSuccess(roomInfo)
} onError: { [weak self] code, message in
guard let self = self else { return }
self.destroyEngineManager()
onError(code, message)
}
}
}
private func updateCameraState() {
if store.roomInfo.isSeatEnabled && !store.currentUser.isOnSeat {
store.videoSetting.isCameraOpened = false
return
}
operateLocalCamera()
}
private func isNeededAutoTakeSeat() -> Bool {
return store.roomInfo.isSeatEnabled && store.currentUser.userId == store.roomInfo.ownerId
}
private func autoTakeSeatForOwner(onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
_ = self.takeSeat() { _,_ in
onSuccess()
} onError: { _, _, code, message in
if code == .alreadyInSeat {
onSuccess()
} else {
onError(code, message)
}
}
}
private func isPushLocalAudioStream(enableAudio: Bool) -> Bool {
if !enableAudio {
return false
}
if store.roomInfo.isMicrophoneDisableForAllUser, store.currentUser.userId != store.roomInfo.ownerId {
return false
}
if store.roomInfo.isSeatEnabled, store.currentUser.userId != store.roomInfo.ownerId {
return false
}
return true
}
private func operateLocalMicrophone(enableAudio: Bool ,onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) {
if isPushLocalAudioStream(enableAudio: enableAudio) {
openLocalMicrophone()
} else if RoomCommon.checkAuthorMicStatusIsDenied() {
muteLocalAudio()
openLocalMicrophone()
}
}
private func operateLocalCamera() {
let openLocalCameraActionBlock = { [weak self] in
guard let self = self else { return }
setLocalVideoView(streamType: .cameraStream, view: nil)
openLocalCamera()
}
if store.videoSetting.isCameraOpened && !store.roomInfo.isCameraDisableForAllUser {
if RoomCommon.checkAuthorCamaraStatusIsDenied() {
openLocalCameraActionBlock()
} else {
RoomCommon.cameraStateActionWithPopCompletion { granted in
if granted {
openLocalCameraActionBlock()
}
}
}
}
}
private func initLocalVideoState() {
setVideoParam()
enableGravitySensor(enable: true)
setGSensorMode(mode: .uiFixLayout)
let resolutionMode: TUIResolutionMode = isLandscape ? .landscape : .portrait
setVideoResolutionMode(streamType: .cameraStream, resolutionMode: resolutionMode)
}
private func setVideoParam() {
setVideoEncoder(videoQuality: store.videoSetting.videoQuality, bitrate: store.videoSetting.videoBitrate,
fps: store.videoSetting.videoFps)
let params = TRTCRenderParams()
params.fillMode = .fill
params.rotation = ._0
setLocalRenderParams(params: params)
}
private func getUserList(nextSequence: Int, localUserList: [UserEntity], onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
roomEngine.getUserList(nextSequence: nextSequence) { [weak self] list, nextSequence in
guard let self = self else { return }
var localUserList = localUserList
list.forEach { userInfo in
if userInfo.userName.isEmpty {
userInfo.userName = userInfo.userId
}
let userModel = UserEntity()
userModel.update(userInfo: userInfo)
localUserList.append(userModel)
}
if nextSequence != 0 {
self.getUserList(nextSequence: nextSequence, localUserList: localUserList, onSuccess: onSuccess, onError: onError)
} else {
self.store.attendeeList = localUserList
onSuccess()
if !self.store.roomInfo.isSeatEnabled {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, param: [:])
}
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewUserList, param: [:])
EngineEventCenter.shared.notifyEngineEvent(event: .onGetUserListFinished, param: [:])
}
} onError: { code, message in
onError(code, message)
debugPrint("getUserList:code:\(code),message:\(message)")
}
}
private func getSeatList(onSuccess: @escaping TUISuccessBlock, onError: @escaping TUIErrorBlock) {
roomEngine.getSeatList { [weak self] seatList in
guard let self = self else { return }
self.store.initialSeatList(seatList: seatList)
self.store.initialOffSeatList()
onSuccess()
} onError: { code, message in
onError(code, message)
debugPrint("getSeatList:code:\(code),message:\(message)")
}
}
private func addEngineObserver() {
roomEngine.addObserver(eventDispatcher)
roomEngine.getTRTCCloud().addDelegate(observer)
conferenceListManager?.addObserver(conferenceListObserver)
}
private func removeEngineObserver() {
roomEngine.removeObserver(eventDispatcher)
roomEngine.getTRTCCloud().removeDelegate(observer)
conferenceListManager?.removeObserver(conferenceListObserver)
}
private func subLogoutNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(handleLogout),
name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
private func unsubLogoutNotification() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
@objc private func handleLogout() {
destroyEngineManager()
}
private func handleDestroyRoomResult() {
destroyEngineManager()
EngineEventCenter.shared.notifyEngineEvent(event: .onDestroyedRoom, param: [ "roomId" : store.roomInfo.roomId ])
}
private func handleExitRoomResult() {
destroyEngineManager()
EngineEventCenter.shared.notifyEngineEvent(event: .onExitedRoom, param: [ "roomId" : store.roomInfo.roomId ])
}
}
// MARK: - TUIExtensionProtocol
extension EngineManager: TUIExtensionProtocol {
func getExtensionInfo(_ key: String, param: [AnyHashable: Any]?) -> [AnyHashable: Any] {
guard let param = param else {
return [:]
}
guard let roomId: String = param["roomId"] as? String else {
return [:]
}
if key == gRoomEngineKey {
return [key: roomEngine]
} else if key == gRoomInfoKey {
return [key: store.roomInfo]
} else if key == gLocalUserInfoKey {
return [key: store.currentUser]
} else {
return [:]
}
}
}
// MARK: - setFramework
extension EngineManager {
fileprivate static let TUIRoomKitFrameworkValue = 1
fileprivate static let TUIRoomKitComponentValue = 18
fileprivate static let IMComponentValue = 19
fileprivate static let TUIRoomKitLanguageValue = 3
private func setFramework() {
let componentValue = store.isImAccess ? EngineManager.IMComponentValue : EngineManager.TUIRoomKitComponentValue
let jsonStr = """
{
"api":"setFramework",
"params":{
"framework":\(EngineManager.TUIRoomKitFrameworkValue),
"component":\(componentValue),
"language":\(EngineManager.TUIRoomKitLanguageValue)
}
}
"""
TUIRoomEngine.callExperimentalAPI(jsonStr: jsonStr)
}
}
private extension String {
static var inAnotherRoomText: String {
localized("You are already in another conference")
}
static var quickConferenceText: String {
localized("'s quick meeting")
}
}

View File

@@ -0,0 +1,29 @@
//
// RequestEntity.swift
// TUIRoomKit
//
// Created by janejntang on 2024/3/20.
//
import Foundation
class RequestEntity {
let requestId: String
let userId: String
var userName: String = ""
var avatarUrl: String = ""
let timestamp: TimeInterval
init(requestId: String, userId: String) {
timestamp = Date().timeIntervalSince1970
self.requestId = requestId
self.userId = userId
guard let userItem = EngineManager.shared.store.attendeeList.first(where: { $0.userId == userId }) else { return }
userName = userItem.userName
avatarUrl = userItem.avatarUrl
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,282 @@
//
// RoomEventDispatcher.swift
// TUIRoomKit
//
// Created by janejntang on 2023/8/14.
//
import Foundation
import RTCRoomEngine
class RoomEventDispatcher: NSObject {
var engineManager: EngineManager {
EngineManager.shared
}
var store: RoomStore {
engineManager.store
}
var roomInfo: TUIRoomInfo {
store.roomInfo
}
var currentUser: UserEntity {
store.currentUser
}
deinit {
debugPrint("deinit")
}
}
extension RoomEventDispatcher: TUIRoomObserver {
// MARK: - Room event callback
func onAllUserMicrophoneDisableChanged(roomId: String, isDisable: Bool) {
roomInfo.isMicrophoneDisableForAllUser = isDisable
let param = [
"roomId" : roomId,
"isDisable" : isDisable,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onAllUserMicrophoneDisableChanged, param: param)
}
func onAllUserCameraDisableChanged(roomId: String, isDisable: Bool) {
roomInfo.isCameraDisableForAllUser = isDisable
let param = [
"roomId" : roomId,
"isDisable" : isDisable,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onAllUserCameraDisableChanged, param: param)
}
func onSendMessageForAllUserDisableChanged(roomId: String, isDisable: Bool) {
roomInfo.isMessageDisableForAllUser = isDisable
}
func onRoomDismissed(roomId: String) {
EngineEventCenter.shared.notifyEngineEvent(event: .onRoomDismissed, param: ["roomId" : roomId,])
}
func onKickedOutOfRoom(roomId: String, reason: TUIKickedOutOfRoomReason, message: String) {
let param = [
"roomId" : roomId,
"reason" : reason,
"message" : message,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onKickedOutOfRoom, param: param)
}
func onRoomNameChanged(roomId: String, roomName: String) {
roomInfo.name = roomName
}
// MARK: - User event callback in the room
func onRemoteUserEnterRoom(roomId: String, userInfo: TUIUserInfo) {
store.remoteUserEnterRoom(userInfo: userInfo)
let param = [
"roomId" : roomId,
"userInfo" : userInfo,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onRemoteUserEnterRoom, param: param)
}
func onRemoteUserLeaveRoom(roomId: String, userInfo: TUIUserInfo) {
store.remoteUserLeaveRoom(userInfo: userInfo)
let param = [
"roomId" : roomId,
"userInfo" : userInfo,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onRemoteUserLeaveRoom, param: param)
}
func onUserRoleChanged(userId: String, userRole: TUIRole) {
userRoleChanged(userId: userId, userRole: userRole)
let param = [
"userId" : userId,
"userRole" : userRole,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onUserRoleChanged, param: param)
}
func onUserVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool, reason: TUIChangeReason) {
userVideoStateChanged(userId: userId, streamType: streamType, hasVideo: hasVideo, reason: reason)
let param = [
"userId" : userId,
"streamType" : streamType,
"hasVideo" : hasVideo,
"reason" : reason,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onUserVideoStateChanged, param: param)
}
func onUserAudioStateChanged(userId: String, hasAudio: Bool, reason: TUIChangeReason) {
userAudioStateChanged(userId: userId, hasAudio: hasAudio, reason: reason)
let param = [
"userId" : userId,
"hasAudio" : hasAudio,
"reason" : reason,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onUserAudioStateChanged, param: param)
}
func onUserVoiceVolumeChanged(volumeMap: [String : NSNumber]) {
userVoiceVolumeChanged(volumeMap: volumeMap)
EngineEventCenter.shared.notifyEngineEvent(event: .onUserVoiceVolumeChanged, param: volumeMap)
}
func onUserScreenCaptureStopped(reason: Int) {
userScreenCaptureStopped()
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_SomeoneSharing, param: ["userId":currentUser.userId, "hasVideo": false])
}
func onSeatListChanged(seatList: [TUISeatInfo], seated seatedList: [TUISeatInfo], left leftList: [TUISeatInfo]) {
seatListChanged(seatList: seatList,seated: seatedList, left: leftList)
let param = [
"seatList": seatList,
"seated": seatedList,
"left": leftList,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onSeatListChanged, param: param)
}
func OnSendMessageForUserDisableChanged(roomId: String, userId: String, isDisable muted: Bool) {
store.updateUserDisableSendingMessage(userId: userId, isDisable: muted)
let param = [
"roomId": roomId,
"userId": userId,
"muted": muted,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onSendMessageForUserDisableChanged, param: param)
}
// MARK: - Signaling request related callbacks
func onRequestReceived(request: TUIRequest) {
requestReceived(request: request)
EngineEventCenter.shared.notifyEngineEvent(event: .onRequestReceived, param: ["request": request,])
}
func onRequestCancelled(requestId: String, userId: String) {
store.deleteTakeSeatRequest(requestId: requestId)
}
func onRequestProcessed(requestId: String, userId: String) {
store.deleteTakeSeatRequest(requestId: requestId)
}
func onKickedOffSeat(userId: String) {
let param = [
"userId": userId,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onKickedOffSeat, param: param)
}
func onKickedOffLine(message: String) {
kickedOffLine()
let param = [
"message": message,
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: .onKickedOffLine, param: param)
}
}
extension RoomEventDispatcher {
private func seatListChanged(seatList: [TUISeatInfo], seated: [TUISeatInfo], left leftList: [TUISeatInfo]) {
store.updateLeftSeatList(leftList: leftList)
store.updateSeatedList(seatList: seated)
}
private func userAudioStateChanged(userId: String, hasAudio: Bool, reason: TUIChangeReason) {
if userId == currentUser.userId {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_CurrentUserHasAudioStream, param: ["hasAudio": hasAudio, "reason": reason])
currentUser.hasAudioStream = hasAudio
}
guard let userModel = store.attendeeList.first(where: { $0.userId == userId }) else { return }
userModel.hasAudioStream = hasAudio
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewUserList, param: [:])
}
private func userVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool, reason: TUIChangeReason) {
switch streamType {
case .screenStream:
if userId == currentUser.userId {
currentUser.hasScreenStream = hasVideo
}
guard let userModel = store.attendeeList.first(where: { $0.userId == userId }) else { return }
userModel.hasScreenStream = hasVideo
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_SomeoneSharing, param: ["userId": userId, "hasVideo": hasVideo])
case .cameraStream:
if userId == currentUser.userId {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_CurrentUserHasVideoStream,
param: ["hasVideo": hasVideo, "reason": reason])
currentUser.hasVideoStream = hasVideo
store.videoSetting.isCameraOpened = hasVideo
}
guard let userModel = store.attendeeList.first(where: { $0.userId == userId }) else { return }
userModel.hasVideoStream = hasVideo
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewUserList, param: [:])
default: break
}
}
private func userRoleChanged(userId: String, userRole: TUIRole) {
let isSelfRoleChanged = userId == currentUser.userId
let isRoomOwnerChanged = userRole == .roomOwner
if let userInfo = store.attendeeList.first(where: { $0.userId == userId }) {
userInfo.userRole = userRole
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewUserList, param: ["userRole": userRole])
}
if isSelfRoleChanged {
store.currentUser.userRole = userRole
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, param: ["userRole": userRole])
}
if isRoomOwnerChanged {
EngineManager.shared.fetchRoomInfo(roomId: roomInfo.roomId) { _ in
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RoomOwnerChanged, param: ["owner": userId])
}
}
if checkAutoTakeSeatForOwner(userId: userId, userRole: userRole) {
let _ = engineManager.takeSeat()
}
if checkAutoSendingMessageForOwner(userId: userId, userRole: userRole) {
engineManager.disableSendingMessageByAdmin(userId: userId, isDisable: false)
}
if isSelfRoleChanged, userRole != .generalUser {
engineManager.updateSeatApplicationList()
}
}
private func checkAutoTakeSeatForOwner(userId: String, userRole: TUIRole) -> Bool {
let isSelfRoleChanged = userId == currentUser.userId
let isRoomOwnerChanged = userRole == .roomOwner
return isSelfRoleChanged && isRoomOwnerChanged && roomInfo.isSeatEnabled && !currentUser.isOnSeat
}
private func checkAutoSendingMessageForOwner(userId: String, userRole: TUIRole) -> Bool {
let isSelfRoleChanged = userId == currentUser.userId
let isRoomOwnerChanged = userRole == .roomOwner
return isSelfRoleChanged && isRoomOwnerChanged && currentUser.disableSendingMessage
}
private func requestReceived(request: TUIRequest) {
switch request.requestAction {
case .takeSeat:
store.addInviteSeatUser(request: request)
default: break
}
}
private func userVoiceVolumeChanged(volumeMap: [String : NSNumber]) {
for (userId, volume) in volumeMap {
guard let userModel = store.attendeeList.first(where: { $0.userId == userId}) else { continue }
userModel.userVoiceVolume = volume.intValue
}
}
private func userScreenCaptureStopped() {
currentUser.hasScreenStream = false
guard let userModel = store.attendeeList.first(where: { $0.userId == currentUser.userId }) else { return }
userModel.hasScreenStream = false
}
private func kickedOffLine() {
engineManager.destroyEngineManager()
}
}

View File

@@ -0,0 +1,268 @@
//
// RoomStore.swift
// TUIRoomKit
//
// Created by aby on 2022/12/28.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import TUICore
import RTCRoomEngine
import Factory
let roomHashNumber: Int = 0x3B9AC9FF
class RoomStore: NSObject {
var currentUser: UserEntity = UserEntity()
var roomInfo: TUIRoomInfo = TUIRoomInfo()
var videoSetting: VideoModel = VideoModel()
var audioSetting: AudioModel = AudioModel()
var attendeeList: [UserEntity] = [] // User list
var seatList: [UserEntity] = [] // List of users who have taken the stage
var offSeatList: [UserEntity] = [] // List of users who not on stage
var inviteSeatList: [RequestEntity] = [] // List of users who apply to be on stage
var isEnteredRoom: Bool = false
var timeStampOnEnterRoom: Int = 0 // Timestamp of entering the meeting
var isImAccess: Bool = false // Whether TUIRoomKit is entered by IM
var selfTakeSeatRequestId: String? // Self ID for applying on stage
var shouldShowFloatChatView = true
private let openCameraKey = "isOpenCamera"
private let openMicrophoneKey = "isOpenMicrophone"
private let shownRaiseHandNoticeKey = "isShownRaiseHandNotice"
weak var conferenceObserver: ConferenceObserver?
var isOpenMicrophone: Bool {
didSet {
UserDefaults.standard.set(isOpenMicrophone, forKey: openMicrophoneKey)
UserDefaults.standard.synchronize()
}
}
var isOpenCamera: Bool {
didSet {
UserDefaults.standard.set(isOpenCamera, forKey: openCameraKey)
UserDefaults.standard.synchronize()
}
}
var isShownRaiseHandNotice: Bool {
didSet {
UserDefaults.standard.set(isShownRaiseHandNotice, forKey: shownRaiseHandNoticeKey)
UserDefaults.standard.synchronize()
}
}
override init() {
if let isOpenMicrophoneValue = UserDefaults.standard.object(forKey: openMicrophoneKey) as? Bool {
isOpenMicrophone = isOpenMicrophoneValue
} else {
isOpenMicrophone = true
}
if let isOpenCameraValue = UserDefaults.standard.object(forKey: openCameraKey) as? Bool {
isOpenCamera = isOpenCameraValue
} else {
isOpenCamera = true
}
if let isShownRaiseHandNoticeValue = UserDefaults.standard.object(forKey: shownRaiseHandNoticeKey) as? Bool {
isShownRaiseHandNotice = isShownRaiseHandNoticeValue
} else {
isShownRaiseHandNotice = true
}
}
func initialRoomInfo(_ roomInfo: TUIRoomInfo) {
self.roomInfo = roomInfo
EngineEventCenter.shared.notifyEngineEvent(event: .onInitialRoomInfo, param: ["roomInfo": roomInfo])
}
func initalEnterRoomMessage() {
isEnteredRoom = true
timeStampOnEnterRoom = Int(Date().timeIntervalSince1970)
}
func initialRoomCurrentUser() {
currentUser.userId = TUILogin.getUserID() ?? ""
currentUser.userName = TUILogin.getNickName() ?? ""
EngineManager.shared.getUserInfo(currentUser.userId) { [weak self] userInfo in
guard let self = self else { return }
guard let userInfo = userInfo else { return }
self.currentUser.update(userInfo: userInfo)
EngineEventCenter.shared.notifyEngineEvent(event: .onInitialSelfUserInfo, param: [:])
} onError: { code, message in
debugPrint("getUserInfo,code:\(code),message:\(message)")
}
}
func initialSeatList(seatList: [TUISeatInfo]) {
var localSeatList = [UserEntity]()
for seatInfo in seatList {
guard let userId = seatInfo.userId, userId != "" else { continue }
guard let userModel = getUserItem(userId) else { continue }
userModel.isOnSeat = true
localSeatList.append(userModel)
}
self.seatList = localSeatList
if seatList.contains(where: { $0.userId == currentUser.userId }) {
updateSelfOnSeatState(isOnSeat: true)
}
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewSeatList, param: [:])
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, param: [:])
}
func initialOffSeatList() {
offSeatList = attendeeList.filter({ !$0.isOnSeat })
}
func updateSelfOnSeatState(isOnSeat: Bool) {
currentUser.isOnSeat = isOnSeat
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_UserOnSeatChanged, param: ["isOnSeat": isOnSeat])
}
func updateUserDisableSendingMessage(userId: String, isDisable: Bool) {
if userId == currentUser.userId {
currentUser.disableSendingMessage = isDisable
}
guard let userItem = getUserItem(userId) else { return }
userItem.disableSendingMessage = isDisable
}
func deleteTakeSeatRequest(requestId: String) {
let requestUserId = inviteSeatList.first(where: { $0.requestId == requestId })?.userId
inviteSeatList = inviteSeatList.filter { requestItem in
requestItem.requestId != requestId
}
EngineEventCenter.shared.notifyEngineEvent(event: .onDeletedTakeSeatRequest, param: ["userId": requestUserId ?? ""])
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewSeatList, param: [:])
}
func deleteInviteSeatUser(_ userId: String) {
inviteSeatList = inviteSeatList.filter { requestItem in
requestItem.userId != userId
}
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewSeatList, param: [:])
}
func addInviteSeatUser(request: TUIRequest) {
guard !inviteSeatList.contains(where: { $0.userId == request.userId }) else { return }
let requestEntity = RequestEntity(requestId: request.requestId, userId: request.userId)
inviteSeatList.append(requestEntity)
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewSeatList, param: [:])
}
func setCameraOpened(_ isCameraOpened: Bool) {
videoSetting.isCameraOpened = isCameraOpened
}
func setSoundOnSpeaker(_ isSoundOnSpeaker: Bool) {
audioSetting.isSoundOnSpeaker = isSoundOnSpeaker
}
func setConferenceObserver(_ observer: ConferenceObserver?) {
conferenceObserver = observer
}
func setInviteSeatList(list: [TUIRequest]) {
inviteSeatList = []
for request in list {
let requestEntity = RequestEntity(requestId: request.requestId, userId: request.userId)
inviteSeatList.append(requestEntity)
}
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewSeatList, param: [:])
}
func updateLeftSeatList(leftList: [TUISeatInfo]) {
guard leftList.count > 0 else { return }
if leftList.contains(where: { $0.userId == currentUser.userId }) {
currentUser.isOnSeat = false
audioSetting.isMicOpened = false
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_UserOnSeatChanged,
param: ["isOnSeat":false])
}
for seatInfo: TUISeatInfo in leftList {
guard let userId = seatInfo.userId else { continue }
if let userItem = attendeeList.first(where: { $0.userId == userId }) {
userItem.isOnSeat = false
addOffSeatItem(userItem)
}
deleteOnSeatItem(userId)
}
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewSeatList, param: [:])
}
func updateSeatedList(seatList: [TUISeatInfo]) {
guard seatList.count > 0 else { return }
if seatList.contains(where: { $0.userId == currentUser.userId }) {
currentUser.isOnSeat = true
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_UserOnSeatChanged,
param: ["isOnSeat":true])
}
for seatInfo: TUISeatInfo in seatList {
guard let userId = seatInfo.userId else { continue }
guard let userInfo = attendeeList.first(where: { $0.userId == userId }) else { continue }
userInfo.isOnSeat = true
addOnSeatItem(userInfo)
deleteOffSeatItem(userId)
}
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewSeatList, param: [:])
}
func remoteUserEnterRoom(userInfo: TUIUserInfo) {
let userItem = UserEntity()
userItem.update(userInfo: userInfo)
addUserItem(userItem)
if roomInfo.isSeatEnabled, !userItem.isOnSeat {
addOffSeatItem(userItem)
}
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewUserList, param: [:])
}
func remoteUserLeaveRoom(userInfo: TUIUserInfo) {
deleteUserItem(userInfo.userId)
deleteOffSeatItem(userInfo.userId)
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewUserList, param: [:])
}
func updateFloatChatShowState(shouldShow: Bool) {
shouldShowFloatChatView = shouldShow
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowFloatChatView, param: ["shouldShow": shouldShow])
}
deinit {
debugPrint("self:\(self),deinit")
}
}
extension RoomStore {
private func getUserItem(_ userId: String) -> UserEntity? {
return attendeeList.first(where: {$0.userId == userId})
}
private func addUserItem(_ userItem: UserEntity) {
guard getUserItem(userItem.userId) == nil else { return }
if userItem.userName.isEmpty {
userItem.userName = userItem.userId
}
attendeeList.append(userItem)
}
private func deleteUserItem(_ userId: String) {
attendeeList.removeAll(where: { $0.userId == userId })
}
private func addOnSeatItem(_ userItem: UserEntity) {
guard !seatList.contains(where: { $0.userId == userItem.userId }) else { return }
seatList.append(userItem)
}
private func deleteOnSeatItem(_ userId: String) {
seatList.removeAll(where: { $0.userId == userId })
}
private func addOffSeatItem(_ userItem: UserEntity) {
guard !offSeatList.contains(where: { $0.userId == userItem.userId }) else { return }
offSeatList.append(userItem)
}
private func deleteOffSeatItem(_ userId: String) {
offSeatList.removeAll(where: { $0.userId == userId })
}
}

View File

@@ -0,0 +1,67 @@
//
// ButtonItemData.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/10.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
class ButtonItemData {
enum ButtonType {
case muteAudioItemType
case muteVideoItemType
case raiseHandItemType
case leaveSeatItemType
case shareScreenItemType
case moreItemType
case switchCamaraItemType
case raiseHandApplyItemType
case normal
}
enum Orientation {
case left
case right
}
var buttonType: ButtonType = .normal
var normalIcon: String = ""
var selectedIcon: String = ""
var disabledIcon: String = ""
var normalTitle: String = ""
var selectedTitle: String = ""
var titleFont: UIFont?
var titleColor: UIColor?
var resourceBundle: Bundle = Bundle.main
var action: ((Any)->Void)?
var normalImage: UIImage? {
return UIImage(named: normalIcon, in: resourceBundle, compatibleWith: nil)?.checkOverturn()
}
var selectedImage: UIImage? {
return UIImage(named: selectedIcon, in: resourceBundle, compatibleWith: nil)?.checkOverturn()
}
var disabledImage: UIImage? {
return UIImage(named: disabledIcon, in: resourceBundle, compatibleWith: nil)?.checkOverturn()
}
var hasNotice: Bool = false
var noticeText: String = ""
var cornerRadius: CGFloat?
var hasLineView: Bool = false
var orientation: Orientation = .left
var imageSize: CGSize?
var size: CGSize?
var backgroundColor: UIColor?
var isSelect: Bool = false
var isEnabled: Bool = true
var isHidden: Bool = false
var alpha: CGFloat = 1
}

View File

@@ -0,0 +1,42 @@
//
// ListCellItemData.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/6.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
class ListCellItemData {
enum ListCellType {
case resolutionType
case frameRateType
case normal
}
var type: ListCellType = .normal
var size: CGSize?
var backgroundColor: UIColor = .clear
//UILabel configuration
var titleText: String = ""
var messageText: String = ""
var titleColor: UIColor?
var messageColor: UIColor?
//UISwitch configuration
var hasSwitch: Bool = false
var isSwitchOn: Bool = false
//UIButton configuration
var hasRightButton: Bool = false
var buttonData: ButtonItemData?
//UISlider configuration
var hasSliderLabel: Bool = false
var hasSlider: Bool = false
var minimumValue: Float = 0
var maximumValue: Float = 100
var sliderStep: Float = 1
var sliderUnit: String = ""
var sliderDefault: Float = 0
var action: ((Any)->Void)?
var hasOverAllAction: Bool = false
var hasDownLineView: Bool = false
}

View File

@@ -0,0 +1,30 @@
//
// PrepareSettingItemData.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/6.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import UIKit
class PrepareSettingItemData {
var titleText: String = ""
var messageText: String = ""
var fieldPlaceholderText: String = ""
var fieldText: String = ""
var fieldEnable: Bool = false
var isSwitchOn: Bool = false
var action: ((Any)->Void)?
var normalIcon: String = ""
var resourceBundle: Bundle = Bundle.main
var hasSwitch: Bool = false
var hasButton: Bool = false
var hasFieldView: Bool = false
var hasOverAllAction: Bool = false
var size: CGSize?
var backgroundColor: UIColor?
var hasDownLineView: Bool = true
}

View File

@@ -0,0 +1,27 @@
//
// TRTCObserver.swift
// TUIRoomKit
//
// Created by janejntang on 2024/4/1.
//
import Foundation
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
class TRTCObserver: NSObject, TRTCCloudDelegate {
var roomId: String {
EngineManager.shared.store.roomInfo.roomId
}
func onExitRoom(_ reason: Int) {
guard reason == 2 else { return }
EngineEventCenter.shared.notifyEngineEvent(event: .onRoomDismissed, param: ["roomId": roomId])
}
func onStatistics(_ statistics: TRTCStatistics) {
EngineEventCenter.shared.notifyEngineEvent(event: .onStatistics, param: ["statistics": statistics])
}
}

View File

@@ -0,0 +1,45 @@
//
// UserEntity.swift
// TUIRoomKit
//
// Created by WesleyLei on 2022/9/26.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
class UserEntity {
var userId: String = ""
var userName: String = ""
var avatarUrl: String = ""
var userRole: TUIRole = .generalUser
var userVoiceVolume: Int = 0
var hasAudioStream: Bool = false
var hasVideoStream: Bool = false
var videoStreamType: TUIVideoStreamType = .cameraStream
var isOnSeat: Bool = false
var disableSendingMessage: Bool = false
var hasScreenStream: Bool = false
func update(userInfo: TUIUserInfo) {
userId = userInfo.userId
userName = userInfo.userName
avatarUrl = userInfo.avatarUrl
userRole = userInfo.userRole
hasAudioStream = userInfo.hasAudioStream
hasVideoStream = userInfo.hasVideoStream
hasScreenStream = userInfo.hasScreenStream
}
init(){}
init(invitation: TUIInvitation) {
self.userId = invitation.invitee.userId
self.userName = invitation.invitee.userName
self.avatarUrl = invitation.invitee.avatarUrl
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,19 @@
//
// VideoModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/3/8.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
class VideoModel {
var isCameraOpened: Bool = true
var videoFps: Int = 15
var videoBitrate: Int = 1_200
var isMirror: Bool = true
var isFrontCamera: Bool = true
var videoQuality: TUIVideoQuality = .quality720P
}

View File

@@ -0,0 +1,38 @@
//
// VideoSeatItem.swift
// TUIVideoSeat
//
// Created by jack on 2023/3/6.
// Copyright © 2023 Tencent. All rights reserved.
import Foundation
import RTCRoomEngine
class VideoSeatItem: Equatable {
static func == (lhs: VideoSeatItem, rhs: VideoSeatItem) -> Bool {
let lhsType = lhs.videoStreamType == .screenStream
let rhsType = rhs.videoStreamType == .screenStream
return (lhs.userId == rhs.userId) && (lhsType == rhsType)
}
var userId: String = ""
var userName: String = ""
var avatarUrl: String = ""
var userRole: TUIRole = .generalUser
var userVoiceVolume: Int = 0
var hasAudioStream: Bool = false
var hasVideoStream: Bool = false
var videoStreamType: TUIVideoStreamType = .cameraStream
var isOnSeat: Bool = false
var disableSendingMessage: Bool = false
var isHasVideoStream: Bool {
return hasVideoStream || videoStreamType == .screenStream
}
func update(userInfo: UserEntity) {
userId = userInfo.userId
userName = userInfo.userName
avatarUrl = userInfo.avatarUrl
userRole = userInfo.userRole
hasAudioStream = userInfo.hasAudioStream
hasVideoStream = userInfo.hasVideoStream
}
}

View File

@@ -0,0 +1,321 @@
//
// ConferenceMainView.swift
// TUIRoomKit
//
// Created by aby on 2022/12/27.
// Copyright © 2022 Tencent. All rights reserved.
// The main conference interface is responsible for arranging and managing the top bar, bottom bar, video interface, etc.
//
import Foundation
import RTCRoomEngine
import Factory
protocol ConferenceMainViewFactory {
func makeBottomView() -> BottomView
func makeTopView() -> TopView
func makeVideoSeatView() -> UIView
func makeRaiseHandNoticeView() -> UIView
func makeLocalAudioView() -> UIView
func makeWaterMarkLayer() -> WaterMarkLayer
func makeFloatChatButton() -> FloatChatButton
func makeFloatChatDisplayView() -> FloatChatDisplayView
func makeRaiseHandApplicationNotificationView() -> RaiseHandApplicationNotificationView
func makeConferencePasswordView() -> ConferencePasswordView
}
struct ConferenceMainViewLayout { //Layout changes when switching between horizontal and vertical screens
let bottomViewLandscapeSpace: Float = 0
let bottomViewPortraitSpace: Float = 34.0
let topViewLandscapeHight: Float = 75.0
let topViewPortraitHight: Float = 105.0
let videoSeatViewPortraitSpace: Float = 73.0
let videoSeatViewLandscapeSpace: Float = 82.0
}
class ConferenceMainView: UIView {
let viewModel: ConferenceMainViewModel
let viewFactory: ConferenceMainViewFactory
let layout: ConferenceMainViewLayout = ConferenceMainViewLayout()
@Injected(\.navigation) private var route
init(viewModel: ConferenceMainViewModel,
viewFactory: ConferenceMainViewFactory) {
self.viewModel = viewModel
self.viewFactory = viewFactory
super.init(frame: .zero)
viewModel.viewResponder = self
subscribeUIEvent()
}
private var currentLandscape: Bool = isLandscape
private let firstDelayDisappearanceTime = 6.0
private let delayDisappearanceTime = 3.0
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var topView: TopView = {
return viewFactory.makeTopView()
}()
lazy var videoSeatView: UIView = {
return viewFactory.makeVideoSeatView()
}()
lazy var bottomView: BottomView = {
return viewFactory.makeBottomView()
}()
lazy var raiseHandNoticeView: UIView = {
return viewFactory.makeRaiseHandNoticeView()
}()
lazy var localAudioView: UIView = {
return viewFactory.makeLocalAudioView()
}()
lazy var waterMarkLayer: CALayer = {
return viewFactory.makeWaterMarkLayer()
}()
lazy var floatChatDisplayView: FloatChatDisplayView = {
return viewFactory.makeFloatChatDisplayView()
}()
lazy var floatChatButton: FloatChatButton = {
return viewFactory.makeFloatChatButton()
}()
lazy var raiseHandApplicationNotificationView: RaiseHandApplicationNotificationView = {
let applicationNotificationView = viewFactory.makeRaiseHandApplicationNotificationView()
return applicationNotificationView
}()
lazy var conferencePasswordView: ConferencePasswordView = {
return viewFactory.makeConferencePasswordView()
}()
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
backgroundColor = UIColor(0x0F1014)
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
override func layoutSubviews() {
super.layoutSubviews()
guard currentLandscape != isLandscape else { return }
setupRootViewOrientation(isLandscape: isLandscape)
currentLandscape = isLandscape
}
func constructViewHierarchy() {
addSubview(videoSeatView)
if viewModel.isShownWaterMark {
layer.addSublayer(waterMarkLayer)
}
addSubview(topView)
addSubview(floatChatDisplayView)
addSubview(floatChatButton)
addSubview(bottomView)
addSubview(localAudioView)
addSubview(raiseHandNoticeView)
addSubview(raiseHandApplicationNotificationView)
addSubview(conferencePasswordView)
}
func activateConstraints() {
setupRootViewOrientation(isLandscape: isLandscape)
raiseHandNoticeView.snp.makeConstraints { make in
make.bottom.equalTo(bottomView.snp.top).offset(-15)
make.centerX.equalToSuperview()
make.height.equalTo(40)
make.width.equalTo(300)
}
localAudioView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.width.height.equalTo(40.scale375())
make.bottom.equalToSuperview().offset(-40.scale375Height())
}
floatChatButton.snp.makeConstraints { make in
make.bottom.equalTo(localAudioView.snp.top).offset(-18)
make.height.equalTo(30)
make.leading.equalTo(videoSeatView.snp.leading)
}
floatChatDisplayView.snp.makeConstraints { make in
make.bottom.equalTo(floatChatButton.snp.top).offset(-8)
make.height.equalTo(128)
make.leading.equalToSuperview().offset(5)
make.width.equalTo(313)
}
raiseHandApplicationNotificationView.snp.makeConstraints { make in
make.top.equalTo(topView.snp.bottom)
make.width.equalTo(359.scale375())
make.centerX.equalToSuperview()
make.height.equalTo(40.scale375Height())
}
conferencePasswordView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func bindInteraction() {
perform(#selector(hideToolBar),with: nil,afterDelay: firstDelayDisappearanceTime)
}
func setupRootViewOrientation(isLandscape: Bool) {
videoSeatView.snp.remakeConstraints { make in
if isLandscape {
make.leading.equalTo(layout.videoSeatViewLandscapeSpace)
make.trailing.equalTo(-layout.videoSeatViewLandscapeSpace)
make.top.bottom.equalToSuperview()
} else {
make.leading.trailing.equalToSuperview()
make.top.equalTo(layout.videoSeatViewPortraitSpace)
make.bottom.equalTo(-layout.videoSeatViewPortraitSpace)
}
}
topView.snp.remakeConstraints() { make in
make.top.equalToSuperview()
make.leading.equalTo(safeAreaLayoutGuide.snp.leading)
make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
if isLandscape {
make.height.equalTo(layout.topViewLandscapeHight)
} else {
make.height.equalTo(layout.topViewPortraitHight)
}
}
bottomView.snp.remakeConstraints { make in
make.leading.equalTo(safeAreaLayoutGuide.snp.leading)
make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
make.height.equalTo(bottomView.isUnfold ? bottomView.unfoldHeight : bottomView.packUpHeight)
if isLandscape {
make.bottom.equalToSuperview().offset(-layout.bottomViewLandscapeSpace)
} else {
make.bottom.equalToSuperview().offset(-layout.bottomViewPortraitSpace)
}
}
topView.updateRootViewOrientation(isLandscape: isLandscape)
setupWaterMarkLayerOrientation(isLandscape: isLandscape)
}
private func setupWaterMarkLayerOrientation(isLandscape: Bool) {
guard viewModel.isShownWaterMark else { return }
let widthSpace = isLandscape ? CGFloat(layout.videoSeatViewLandscapeSpace) : 0
let heightSpace = isLandscape ? 0 : CGFloat(layout.videoSeatViewPortraitSpace)
waterMarkLayer.frame = CGRect(x: widthSpace, y: heightSpace, width: kScreenWidth - widthSpace * 2, height: kScreenHeight - heightSpace * 2)
waterMarkLayer.setNeedsDisplay()
}
private func subscribeUIEvent() {
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowFloatChatView, responder: self)
}
private func unsubscribeEvent() {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowFloatChatView, responder: self)
}
deinit {
NSObject.cancelPreviousPerformRequests(withTarget: self)
unsubscribeEvent()
debugPrint("deinit \(self)")
}
}
extension ConferenceMainView: ConferenceMainViewResponder {
func hidePasswordView() {
conferencePasswordView.hide()
}
func showPasswordView(roomId: String) {
conferencePasswordView.show(roomId: roomId)
}
func showExitRoomView() {
let view = ExitRoomView(viewModel: ExitRoomViewModel())
view.show(rootView: self)
}
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 showAlertWithAutoConfirm(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?, autoConfirmSeconds: Int?) {
RoomRouter.presentAlert(title: title, message: message, sureTitle: sureTitle, declineTitle: declineTitle, sureBlock: sureBlock, declineBlock: declineBlock, autoConfirmSeconds: autoConfirmSeconds)
}
func makeToast(text: String) {
RoomRouter.makeToastInCenter(toast: text, duration: 1)
}
func showRaiseHandNoticeView() {
raiseHandNoticeView.isHidden = false
}
func updateRoomInfo(roomInfo: TUIRoomInfo) {
floatChatButton.updateRoomId(roomId: roomInfo.roomId)
}
private func showToolBar() {
topView.alpha = 1
bottomView.alpha = 1
topView.isHidden = false
bottomView.isHidden = false
viewModel.hideLocalAudioView()
}
@objc private func hideToolBar() {
topView.alpha = 0
bottomView.alpha = 0
topView.isHidden = true
bottomView.isHidden = true
viewModel.showLocalAudioView()
}
func changeToolBarHiddenState() {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideToolBar), object: nil)
if topView.isHidden {
showToolBar()
perform(#selector(hideToolBar),with: nil,afterDelay: delayDisappearanceTime)
} else if !bottomView.isUnfold {
hideToolBar()
}
}
func setToolBarDelayHidden(isDelay: Bool) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideToolBar), object: nil)
guard !bottomView.isUnfold, isDelay else { return }
perform(#selector(hideToolBar),with: nil,afterDelay: delayDisappearanceTime)
}
func showRepeatJoinRoomAlert() {
let sureAction = UIAlertAction(title: .repeatJoinRoomSureText, style: .default) { _ in
}
let alertState = AlertState(title: .repeatJoinRoomTitle, message: .repeatJoinRoomMessage, sureAction: sureAction, declineAction: nil)
route.present(route: .alert(state: alertState))
}
}
extension ConferenceMainView: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_ShowFloatChatView:
guard let shouldShow = info?["shouldShow"] as? Bool else { return }
floatChatButton.isHidden = !shouldShow
floatChatDisplayView.isHidden = !shouldShow
default: break
}
}
}
private extension String {
static let repeatJoinRoomTitle = localized("Currently in the room")
static let repeatJoinRoomMessage = localized("Please exit before joining a new room")
static let repeatJoinRoomSureText = localized("I see")
}

View File

@@ -0,0 +1,442 @@
//
// RoomRouter.swift
// TUIRoomKit
//
// Created by janejntang on 2022/9/30.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import TUICore
import RTCRoomEngine
// View routing context
class RouteContext {
var rootNavigation: UINavigationController?
typealias Weak<T> = () -> T?
var alterControllers: [Weak<UIViewController>] = []
var popUpViewController: Weak<PopUpViewController>?
var appearance: AnyObject?
let navigationDelegate = RoomRouter.RoomNavigationDelegate()
var currentLandscape: Bool = isLandscape
weak var rootViewController: UIViewController?
var chatWindow : UIWindow?
let chatWindowWidth = min(kScreenWidth, kScreenHeight) + 20
let chatWindowHeight = min(kScreenWidth, kScreenHeight)
init() {
if #available(iOS 13, *) {
appearance = UINavigationBarAppearance()
}
}
}
class RoomRouter: NSObject {
static let shared = RoomRouter()
private let context: RouteContext = RouteContext()
private override init() {
super.init()
subscribeUIEvent()
}
class RoomNavigationDelegate: NSObject {
}
var navController: UINavigationController? {
return context.rootNavigation
}
private func subscribeUIEvent() {
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowRoomMainView, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowRoomVideoFloatView, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_HiddenChatWindow, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
NotificationCenter.default.addObserver(self, selector: #selector(handleDeviceOrientationChange), name: UIDevice.orientationDidChangeNotification, object: nil)
}
private func unsubscribeEvent() {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowRoomMainView, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowRoomVideoFloatView, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_HiddenChatWindow, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
}
func hasChatWindow() -> Bool {
return context.chatWindow != nil
}
func pushToChatController(user: UserEntity, roomInfo: TUIRoomInfo) {
guard let chatVC = makeChatController(user: user, roomInfo: roomInfo) else { return }
if !isLandscape {
push(viewController: chatVC, animated: false)
} else {
let nav = UINavigationController(rootViewController: chatVC)
nav.navigationBar.backgroundColor = .white
if #available(iOS 13, *) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
context.chatWindow = UIWindow(windowScene: windowScene)
} else {
context.chatWindow = UIWindow(frame: UIScreen.main.bounds)
}
context.chatWindow?.frame = CGRect(x: kScreenWidth - context.chatWindowWidth - kDeviceSafeBottomHeight, y: 0, width: context.chatWindowWidth, height: context.chatWindowHeight)
context.chatWindow?.rootViewController = nav
context.chatWindow?.windowLevel = UIWindow.Level.statusBar + 1
context.chatWindow?.isHidden = false
context.chatWindow?.makeKeyAndVisible()
}
}
func makeChatController(user: UserEntity, roomInfo: TUIRoomInfo) -> UIViewController? {
let config: [String : Any] = [
TUICore_TUIChatService_SetChatExtensionMethod_EnableVideoCallKey: false,
TUICore_TUIChatService_SetChatExtensionMethod_EnableAudioCallKey: false,
TUICore_TUIChatService_SetChatExtensionMethod_EnableLinkKey: false,
]
TUICore.callService(TUICore_TUIChatService, method: TUICore_TUIChatService_SetChatExtensionMethod, param: config)
let maxSizeKey = "TUICore_TUIChatService_SetMaxTextSize"
let chatWidth = min(kScreenWidth, kScreenHeight)
let sizeParam : [String : Any] = ["maxsize": CGSize(width: chatWidth - 150, height: Double(MAXFLOAT))]
TUICore.callService(TUICore_TUIChatService, method: maxSizeKey, param: sizeParam)
let param : [String : Any] = [
TUICore_TUIChatObjectFactory_ChatViewController_Title : String.chatText,
TUICore_TUIChatObjectFactory_ChatViewController_GroupID: roomInfo.roomId,
TUICore_TUIChatObjectFactory_ChatViewController_AvatarUrl : user.avatarUrl,
TUICore_TUIChatObjectFactory_ChatViewController_Enable_Video_Call : String(0),
TUICore_TUIChatObjectFactory_ChatViewController_Enable_Audio_Call : String(0),
TUICore_TUIChatObjectFactory_ChatViewController_Enable_Room : String(0),
TUICore_TUIChatObjectFactory_ChatViewController_Limit_Portrait_Orientation: String(1),
TUICore_TUIChatObjectFactory_ChatViewController_Enable_Poll : String(0),
TUICore_TUIChatObjectFactory_ChatViewController_Enable_GroupNote : String(0),
TUICore_TUIChatObjectFactory_ChatViewController_Enable_WelcomeCustomMessage :String(0),
TUICore_TUIChatObjectFactory_ChatViewController_Enable_TakePhoto :String(0),
TUICore_TUIChatObjectFactory_ChatViewController_Enable_RecordVideo :String(0),
]
return TUICore.createObject(TUICore_TUIChatObjectFactory, key: TUICore_TUIChatObjectFactory_ChatViewController_Classic,
param: param) as? UIViewController
}
func pushMainViewController() {
let vc = makeMainViewController()
push(viewController: vc)
}
func presentPopUpViewController(viewType: PopUpViewType, height: CGFloat, backgroundColor: UIColor = UIColor(0x1B1E26)) {
if let observer = context.popUpViewController, let vc = observer() {
vc.dismiss(animated: false)
}
let vc = makePopUpViewController(viewType: viewType, height: height, backgroundColor: backgroundColor)
let weakObserver = { [weak vc] in return vc }
context.popUpViewController = weakObserver
present(viewController: vc)
}
func dismissPopupViewController(completion: (() -> Void)? = nil) {
guard let observer = context.popUpViewController, let vc = observer() else {
completion?()
return
}
vc.viewModel.changeSearchControllerActive()
vc.dismiss(animated: true, completion: completion)
context.popUpViewController = nil
}
func pop(animated: Bool = true) {
guard let viewControllerArray = navController?.viewControllers else { return }
if viewControllerArray.count <= 1 {
viewControllerArray.first?.dismiss(animated: true)
navController?.dismiss(animated: true)
context.rootNavigation = nil
} else {
if let vc = viewControllerArray.last, vc is ConferenceMainViewController {
navController?.popViewController(animated: animated)
context.rootNavigation = nil
} else {
navController?.popViewController(animated: animated)
}
}
}
func popToRoomEntranceViewController() {
if let navController = navController {
var controllerArray = navController.viewControllers
controllerArray.reverse()
for vc in controllerArray {
if vc is PopUpViewController {
vc.dismiss(animated: true)
} else {
pop()
}
if vc is ConferenceMainViewController {
break
}
}
} else if let vc = context.rootViewController {
vc.dismiss(animated: true)
}
}
class func presentAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?, autoConfirmSeconds: Int? = nil) {
var timer: Timer?
var remainingSeconds = autoConfirmSeconds ?? 0
let alertVC = UIAlertController(title: title,
message: message,
preferredStyle: .alert)
if let declineTitle = declineTitle {
let declineAction = UIAlertAction(title: declineTitle, style: .destructive) { _ in
declineBlock?()
}
declineAction.setValue(UIColor(0x4F586B), forKey: "titleTextColor")
alertVC.addAction(declineAction)
}
let sureActionTitle = (autoConfirmSeconds != nil) ? "\(sureTitle ?? "") (\(remainingSeconds))" : sureTitle
let sureAction = UIAlertAction(title: sureActionTitle, style: .default) { _ in
sureBlock?()
timer?.invalidate()
}
alertVC.addAction(sureAction)
if autoConfirmSeconds != nil {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
remainingSeconds -= 1
if remainingSeconds <= 0 {
sureBlock?()
timer?.invalidate()
} else {
sureAction.setValue("\(sureTitle ?? "") (\(remainingSeconds))", forKey: "title")
}
}
}
shared.getCurrentWindowViewController()?.present(alertVC, animated: true)
let weakObserver = { [weak alertVC] in return alertVC }
shared.context.alterControllers.append(weakObserver)
}
func dismissAllAlertController(complete: @escaping (()->())) {
guard context.alterControllers.count > 0 else {
complete()
return
}
dismissAlertController(index: context.alterControllers.count - 1) { [weak self] in
guard let self = self else { return }
self.context.alterControllers = []
complete()
}
}
private func dismissAlertController(index: Int, complete: @escaping (()->())) {
if index < 0 {
complete()
return
}
if let observer = context.alterControllers[safe: index], let vc = observer() {
vc.dismiss(animated: true) { [weak self] in
guard let self = self else { return }
self.dismissAlertController(index: index - 1, complete: complete)
}
} else {
dismissAlertController(index: index-1, complete: complete)
}
}
class func makeToast(toast: String) {
shared.getCurrentWindowViewController()?.view.makeToast(toast)
}
class func makeToastInCenter(toast: String, duration:TimeInterval) {
guard let windowView = shared.getCurrentWindowViewController()?.view else {return}
windowView.makeToast(toast,duration: duration,position:TUICSToastPositionCenter)
}
class func makeToastInWindow(toast: String, duration:TimeInterval) {
guard let window = RoomRouter.getCurrentWindow() else {return}
window.makeToast(toast,duration: duration,position:TUICSToastPositionCenter)
}
class func getCurrentWindow() -> UIWindow? {
var windows: [UIWindow]
if #available(iOS 13.0, *), let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
windows = windowScene.windows
} else {
windows = UIApplication.shared.windows
}
if let keyWindow = windows.first(where: { $0.isKeyWindow }) {
return keyWindow
} else {
return windows.last(where: { $0.windowLevel == .normal && $0.isHidden == false &&
CGRectEqualToRect($0.bounds , UIScreen.main.bounds) })
}
}
func initializeNavigationController(rootViewController: UIViewController) {
guard context.rootNavigation == nil else { return }
if let nav = rootViewController.navigationController {
context.rootNavigation = nav
}
context.rootViewController = rootViewController
}
@objc func handleDeviceOrientationChange() {
guard context.currentLandscape != isLandscape else { return }
destroyChatWindow()
context.currentLandscape = isLandscape
}
deinit {
unsubscribeEvent()
debugPrint("deinit \(self)")
}
}
extension RoomRouter {
func push(viewController: UIViewController, animated: Bool = true) {
guard let navController = navController else {
createRootNavigationAndPresent(controller: viewController)
return
}
navController.pushViewController(viewController, animated: animated)
}
func present(viewController: UIViewController, animated: Bool = true) {
if #available(iOS 13.0, *) {
viewController.modalPresentationStyle = .automatic
} else {
viewController.modalPresentationStyle = .overFullScreen
}
if let navController = navController {
navController.present(viewController, animated: animated)
} else if let vc = context.rootViewController {
vc.present(viewController, animated: true)
}
}
private func createRootNavigationAndPresent(controller: UIViewController) {
let navigationController = RoomKitNavigationController(rootViewController: controller)
navigationController.modalPresentationStyle = .fullScreen
context.rootNavigation = navigationController
if #available(iOS 13.0, *) {
setupNavigationBarAppearance()
if let appearance = context.appearance as? UINavigationBarAppearance {
navigationController.navigationBar.standardAppearance = appearance
navigationController.navigationBar.scrollEdgeAppearance = appearance
}
} else {
navigationController.navigationBar.shadowImage = UIImage()
navigationController.navigationBar.barStyle = .default
}
let weakObserver = { [weak navigationController] in
return navigationController
}
guard let controller = getCurrentWindowViewController() else { return }
controller.present(navigationController, animated: true)
}
@available(iOS 13.0, *)
private func setupNavigationBarAppearance() {
guard let barAppearance = context.appearance as? UINavigationBarAppearance else {
return
}
barAppearance.configureWithDefaultBackground()
barAppearance.shadowColor = nil
barAppearance.backgroundEffect = nil
barAppearance.backgroundColor = .white
}
private func getCurrentWindowViewController() -> UIViewController? {
var keyWindow: UIWindow?
for window in UIApplication.shared.windows {
if window.isMember(of: UIWindow.self), window.isKeyWindow {
keyWindow = window
break
}
}
guard let rootController = keyWindow?.rootViewController else {
return nil
}
func findCurrentController(from vc: UIViewController?) -> UIViewController? {
if let nav = vc as? UINavigationController {
return findCurrentController(from: nav.topViewController)
} else if let tabBar = vc as? UITabBarController {
return findCurrentController(from: tabBar.selectedViewController)
} else if let presented = vc?.presentedViewController {
return findCurrentController(from: presented)
}
return vc
}
let viewController = findCurrentController(from: rootController)
return viewController
}
private func makeMainViewController() -> UIViewController {
let controller = ConferenceMainViewController()
return controller
}
private func makePopUpViewController(viewType: PopUpViewType, height: CGFloat, backgroundColor: UIColor) -> PopUpViewController {
let controller = PopUpViewController(popUpViewModelFactory: self, viewType: viewType, height: height, backgroundColor: backgroundColor)
return controller
}
private func destroyChatWindow() {
guard context.chatWindow != nil else { return }
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.context.chatWindow = nil
}
}
}
extension RoomRouter.RoomNavigationDelegate: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
if viewController is ConferenceMainViewController {
if #available(iOS 13.0, *) {
if let appearance = RoomRouter.shared.context.appearance as? UINavigationBarAppearance {
navigationController.navigationBar.standardAppearance = appearance
navigationController.navigationBar.scrollEdgeAppearance = appearance
}
}
}
}
}
extension RoomRouter: PopUpViewModelFactory {
func makeRootViewModel(viewType: PopUpViewType, height: CGFloat, backgroundColor: UIColor) -> PopUpViewModel {
let viewModel = PopUpViewModel(viewType: viewType, height: height)
viewModel.backgroundColor = backgroundColor
return viewModel
}
}
extension RoomRouter: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_ShowRoomVideoFloatView:
dismissPopupViewController()
popToRoomEntranceViewController()
RoomVideoFloatView.show()
case .TUIRoomKitService_ShowRoomMainView:
RoomVideoFloatView.dismiss()
self.pushMainViewController()
case .TUIRoomKitService_HiddenChatWindow:
destroyChatWindow()
case .TUIRoomKitService_DismissConferenceViewController:
dismissAllAlertController() { [weak self] in
guard let self = self else { return }
self.dismissPopupViewController() { [weak self] in
guard let self = self else { return }
self.popToRoomEntranceViewController()
}
}
default: break
}
}
}
private extension String {
static var chatText: String {
localized("Chat")
}
}

View File

@@ -0,0 +1,12 @@
//
// ConferenceMainViewState.swift
// TUIRoomKit
//
// Created by janejntang on 2024/9/3.
//
import Foundation
struct ConferenceMainViewState: Codable {
var isInternalCreation = false
}

View File

@@ -0,0 +1,63 @@
//
// ConferenceMainViewStore.swift
// TUIRoomKit
//
// Created by janejntang on 2024/9/3.
//
import Foundation
import Combine
protocol ConferenceMainViewStore {
var isInternalCreation: Bool { get }
func updateInternalCreation(isInternalCreation: Bool)
func dispatch(action: Action)
func select<Value: Equatable>(_ selector: Selector<ConferenceMainViewState, Value>) -> AnyPublisher<Value, Never>
func selectCurrent<Value>(_ selector: Selector<ConferenceMainViewState, Value>) -> Value
}
class ConferenceMainViewStoreProvider {
static let updateInternalCreation = ActionTemplate(id: "updateInternalCreation", payloadType: Bool.self)
private(set) lazy var store: Store<ConferenceMainViewState, Void> = Store(initialState: ConferenceMainViewState())
private let conferenceMainViewReducer = Reducer<ConferenceMainViewState>(
ReduceOn(updateInternalCreation) { state,action in
state.isInternalCreation = action.payload
}
)
init() {
initStore()
}
deinit {
store.unregister(reducer: conferenceMainViewReducer)
}
private func initStore() {
store.register(reducer: conferenceMainViewReducer)
}
}
extension ConferenceMainViewStoreProvider: ConferenceMainViewStore {
var isInternalCreation: Bool {
return store.state.isInternalCreation
}
func updateInternalCreation(isInternalCreation: Bool) {
store.dispatch(action: ConferenceMainViewStoreProvider.updateInternalCreation(payload: isInternalCreation))
}
func dispatch(action: Action) {
store.dispatch(action: action)
}
func select<Value>(_ selector: Selector<ConferenceMainViewState, Value>) -> AnyPublisher<Value, Never> where Value : Equatable {
return store.select(selector)
.removeDuplicates()
.eraseToAnyPublisher()
}
func selectCurrent<Value>(_ selector: Selector<ConferenceMainViewState, Value>) -> Value {
return store.selectCurrent(selector)
}
}

View File

@@ -0,0 +1,17 @@
//
// ConferenceMainViewStoreRegister.swift
// TUIRoomKit
//
// Created by janejntang on 2024/9/3.
//
import Factory
extension Container {
var conferenceMainViewStore: Factory<ConferenceMainViewStore> {
Factory(self) {
ConferenceMainViewStoreProvider()
}
.shared
}
}

View File

@@ -0,0 +1,145 @@
//
// BottomItemView.swift
// Alamofire
//
// Created by aby on 2022/12/23.
// Copyright © 2023 Tencent. All rights reserved.
//
import UIKit
class BottomItemView: UIView {
var itemData: ButtonItemData
var engineManager: EngineManager {
EngineManager.shared
}
let button: UIButton = {
let button = UIButton(type: .custom)
return button
}()
let label: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 10.0)
label.textColor = UIColor(0xD1D9EC)
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
return label
}()
let imageView: UIImageView = {
let view = UIImageView()
return view
}()
let noticeView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0xED414D)
view.layer.borderWidth = 3
view.layer.borderColor = UIColor(0x2A2D38).cgColor
view.layer.cornerRadius = 12
view.clipsToBounds = true
return view
}()
let noticeLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xFFFFFF)
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
label.backgroundColor = .clear
return label
}()
// MARK: - initialized function
init(itemData: ButtonItemData) {
self.itemData = itemData
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
self.layer.cornerRadius = 10
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(button)
button.addSubview(imageView)
button.addSubview(label)
button.addSubview(noticeView)
noticeView.addSubview(noticeLabel)
}
func activateConstraints() {
button.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
imageView.snp.makeConstraints { make in
if itemData.normalTitle.isEmpty, itemData.selectedTitle.isEmpty {
make.centerY.equalToSuperview()
} else {
make.top.equalToSuperview().offset(7)
}
make.width.height.equalTo(24)
make.centerX.equalToSuperview()
}
label.snp.makeConstraints { make in
if itemData.normalIcon.isEmpty, itemData.selectedIcon.isEmpty {
make.centerY.equalToSuperview()
} else {
make.top.equalTo(imageView.snp.bottom).offset(2)
}
make.width.equalToSuperview()
make.height.equalTo(14)
}
noticeLabel.snp.makeConstraints { make in
make.trailing.equalTo(button).offset(-7)
make.top.equalToSuperview().offset(4)
make.width.height.greaterThanOrEqualTo(16)
}
noticeView.snp.makeConstraints { make in
make.leading.top.equalTo(noticeLabel).offset(-4)
make.trailing.bottom.equalTo(noticeLabel).offset(4)
make.width.lessThanOrEqualTo(button)
}
}
func bindInteraction() {
setupViewState(item: itemData)
button.addTarget(self, action: #selector(clickMenuButton(sender:)), for: .touchUpInside)
}
func setupViewState(item: ButtonItemData) {
itemData = item
button.isSelected = item.isSelect
button.isEnabled = item.isEnabled
imageView.image = item.isSelect ? itemData.selectedImage : itemData.normalImage
label.text = item.isSelect ? itemData.selectedTitle : itemData.normalTitle
button.alpha = item.alpha
noticeView.isHidden = !item.hasNotice
noticeLabel.text = item.noticeText
}
@objc
func clickMenuButton(sender: UIView) {
itemData.action?(sender)
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,245 @@
//
// BottomView.swift
// TUIRoomKit
//
// Created by aby on 2022/12/21.
// Copyright © 2022 Tencent. All rights reserved.
//
import UIKit
class BottomView: UIView {
// MARK: - store property
let viewModel: BottomViewModel
private var viewArray: [BottomItemView] = []
var isUnfold: Bool = false
let unfoldHeight = Float(130.scale375Height())
let packUpHeight = Float(68.scale375Height())
let baseButtonMenuView: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.alignment = .center
view.distribution = .equalSpacing
view.spacing = 10
return view
}()
let moreButtonMenuView: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.alignment = .center
view.distribution = .equalSpacing
view.spacing = 10
return view
}()
let buttonMenuView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x0F1014)
view.layer.cornerRadius = 12
return view
}()
let backgroundView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x0F1014)
return view
}()
// MARK: - initialized function
init(viewModel: BottomViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(backgroundView)
addSubview(buttonMenuView)
buttonMenuView.addSubview(moreButtonMenuView)
buttonMenuView.addSubview(baseButtonMenuView)
moreButtonMenuView.isHidden = true
setupMenuStackView(items: viewModel.viewItems)
layoutMoreButtonMenu()
}
func setupMenuStackView(items: [ButtonItemData]) {
for i in 0...(items.count - 1) {
guard let item = viewModel.viewItems[safe: i] else { continue }
let view = BottomItemView(itemData: item)
let size = item.size ?? CGSize(width: 52.scale375(), height: 52.scale375())
view.snp.makeConstraints { make in
make.height.equalTo(size.height)
make.width.equalTo(size.width)
}
view.backgroundColor = item.backgroundColor ?? UIColor(0x2A2D38)
viewArray.append(view)
if i < 6 {
baseButtonMenuView.addArrangedSubview(view)
} else {
moreButtonMenuView.addArrangedSubview(view)
}
}
}
func layoutMoreButtonMenu() {
let emptyViewCount = baseButtonMenuView.subviews.count - moreButtonMenuView.subviews.count
if emptyViewCount <= 0 {return}
for _ in 1...emptyViewCount {
let emptyView = BottomItemView(itemData: ButtonItemData())
emptyView.snp.makeConstraints { make in
make.height.equalTo(52.scale375())
make.width.equalTo(52.scale375())
moreButtonMenuView.addArrangedSubview(emptyView)
}
viewArray.append(emptyView)
}
}
func activateConstraints() {
backgroundView.snp.makeConstraints { make in
make.bottom.leading.trailing.equalToSuperview()
make.height.equalTo(packUpHeight)
}
let width = min(kScreenWidth, kScreenHeight)
buttonMenuView.snp.makeConstraints { make in
make.width.equalTo(width)
make.bottom.centerX.height.equalToSuperview()
}
baseButtonMenuView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(8.scale375())
make.height.equalTo(52.scale375())
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
}
moreButtonMenuView.snp.makeConstraints { make in
make.bottom.equalToSuperview()
make.height.equalTo(52.scale375())
make.leading.trailing.equalTo(baseButtonMenuView)
}
}
func bindInteraction() {
viewModel.viewResponder = self
}
deinit {
debugPrint("deinit \(self)")
}
}
extension BottomView: BottomViewModelResponder {
func updateButtonView(item: ButtonItemData) {
guard let view = viewArray.first(where: { $0.itemData.buttonType == item.buttonType }) else { return }
view.setupViewState(item: item)
}
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 updateStackView(items: [ButtonItemData]) {
viewArray.forEach { view in
view.removeFromSuperview()
}
viewArray = []
setupMenuStackView(items: items)
layoutMoreButtonMenu()
}
func makeToast(text: String) {
RoomRouter.makeToastInCenter(toast: text, duration: 1)
}
private func updateBottomViewConstraints(isUnfold: Bool, completion: @escaping () -> Void) {
UIView.animate(withDuration: 0.3) { [weak self] () in
guard let self = self else { return }
self.snp.updateConstraints { make in
make.height.equalTo(isUnfold ? self.unfoldHeight : self.packUpHeight)
}
self.superview?.layoutIfNeeded()
} completion: { _ in
completion()
}
}
func updataBottomView(isUp: Bool) {
buttonMenuView.backgroundColor = isUp ? UIColor(0x2A2D38) : UIColor(0x0F1014)
self.isUnfold = isUp
if isUp {
updateBottomViewConstraints(isUnfold: true) { [weak self] in
guard let self = self else { return }
self.moreButtonMenuView.isHidden = false
}
} else {
moreButtonMenuView.isHidden = true
updateBottomViewConstraints(isUnfold: false) {}
}
}
}
private extension String {
static var leaveRoomTitle: String {
localized("Are you sure you want to leave the conference?")
}
static var destroyRoomTitle: String {
localized("Are you sure you want to end the conference?")
}
static var destroyRoomCancelTitle: String {
localized("Wait")
}
static var logoutOkText: String {
localized("OK")
}
static var dismissMeetingTitle: String {
localized("If you don't want to end the conference")
}
static var appointNewHostText: String {
localized("Please appoint a new host before leaving the conference")
}
static var leaveMeetingText: String {
localized("Leave conference")
}
static var dismissMeetingText: String {
localized("End conference")
}
static var cancelText: String {
localized("Cancel")
}
static var toastTitleText: String {
localized("Share Screen")
}
static var toastMessageText: String {
localized("Stop TUIRoom screen sharing screen live?")
}
static var toastStopText: String {
localized("Stop")
}
}

View File

@@ -0,0 +1,213 @@
//
// ConferencePasswordView.swift
// TUIRoomKit
//
// Created by janejntang on 2024/7/30.
//
import Foundation
import Factory
import RTCRoomEngine
class ConferencePasswordView: UIView {
var roomId: String?
private let maxNumber = 6
weak var viewModel: ConferenceMainViewModel?
let shieldingView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x0F1014).withAlphaComponent(0.7)
return view
}()
let contentView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0xFFFFFF)
view.layer.cornerRadius = 10
return view
}()
let titleLabel: UILabel = {
let view = UILabel()
view.text = .conferencePassword
view.backgroundColor = .clear
view.textColor = UIColor(0x0F1014)
view.font = UIFont.systemFont(ofSize: 16, weight: .medium)
view.textAlignment = .center
return view
}()
lazy var textField: UITextField = {
let view = UITextField(frame: .zero)
view.backgroundColor = .clear
view.placeholder = .pleaseEnterTheConferencePassword
view.textColor = UIColor(0x2B2E38)
view.tintColor = UIColor(0x2B2E38).withAlphaComponent(0.7)
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.keyboardType = .numberPad
view.textAlignment = isRTL ? .right : .left
view.layer.cornerRadius = 10
view.layer.borderWidth = 1
view.layer.borderColor = UIColor(0x1C66E5).cgColor
view.delegate = self
view.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
view.leftViewMode = .always
let deleteButton = UIButton(type: .system)
deleteButton.frame = CGRect(x: 0, y: 0, width: 60, height: 30)
deleteButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 20, bottom: 5, right: 20)
deleteButton.setImage(UIImage(named: "room_cancel", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
deleteButton.addTarget(self, action: #selector(deleteAction(sender:)), for: .touchUpInside)
view.rightView = deleteButton
view.rightViewMode = .whileEditing
return view
}()
let cancelButton: UIButton = {
let button = UIButton()
button.setTitle(.cancel, for: .normal)
button.setTitleColor(UIColor(0x4F586B), for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .regular)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor(0xD5E0F2).withAlphaComponent(0.5).cgColor
return button
}()
let sureButton: UIButton = {
let button = UIButton()
button.setTitle(.join, for: .normal)
button.setTitleColor(UIColor(0x1C66E5), for: .normal)
button.setTitleColor(UIColor(0x1C66E5).withAlphaComponent(0.5), for: .disabled)
button.layer.borderWidth = 0.5
button.layer.borderColor = UIColor(0xD5E0F2).withAlphaComponent(0.5).cgColor
return button
}()
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
isViewReady = true
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
private func constructViewHierarchy() {
addSubview(shieldingView)
addSubview(contentView)
contentView.addSubview(titleLabel)
contentView.addSubview(textField)
contentView.addSubview(cancelButton)
contentView.addSubview(sureButton)
}
private func activateConstraints() {
shieldingView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalTo(323.scale375())
make.height.equalTo(180.scale375Height())
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(24.scale375Height())
make.centerX.equalToSuperview()
}
textField.snp.makeConstraints { make in
make.height.equalTo(40.scale375Height())
make.width.equalTo(298.scale375())
make.centerX.equalToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(14.scale375Height())
}
cancelButton.snp.makeConstraints { make in
make.width.equalToSuperview().multipliedBy(0.5)
make.leading.equalToSuperview()
make.height.equalTo(54.scale375Height())
make.bottom.equalToSuperview()
}
sureButton.snp.makeConstraints { make in
make.width.equalToSuperview().multipliedBy(0.5)
make.trailing.equalToSuperview()
make.height.equalTo(54.scale375Height())
make.bottom.equalToSuperview()
}
}
private func bindInteraction() {
cancelButton.addTarget(self, action: #selector(cancelAction(sender:)), for: .touchUpInside)
sureButton.addTarget(self, action: #selector(sureAction(sender:)), for: .touchUpInside)
updateSureButton()
}
private func updateSureButton() {
guard let text = textField.text else { return }
sureButton.isEnabled = text.count > 0
}
@objc func cancelAction(sender: UIButton) {
guard superview != nil else { return }
removeFromSuperview()
guard let roomId = roomId else { return }
viewModel?.handleWrongPasswordFault(roomId: roomId)
}
@objc func sureAction(sender: UIButton) {
guard superview != nil else { return }
viewModel?.joinConferenceParams?.password = textField.text
viewModel?.joinConference()
}
func hide() {
self.isHidden = true
textField.resignFirstResponder()
}
func show(roomId: String) {
self.roomId = roomId
self.isHidden = false
}
@objc func deleteAction(sender: UIButton) {
textField.text = ""
sureButton.isEnabled = false
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
guard layer.contains(point) else { return }
textField.resignFirstResponder()
}
deinit {
debugPrint("deinit:\(self)")
}
@Injected(\.conferenceStore) private var store
}
extension ConferencePasswordView: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.isSecureTextEntry = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
textField.isSecureTextEntry = false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else { return true }
let newText = NSString(string: text).replacingCharacters(in: range, with: string)
sureButton.isEnabled = newText.count > 0
return newText.count <= maxNumber
}
}
private extension String {
static let conferencePassword = localized("Conference password")
static let join = localized("Join")
static let cancel = localized("Cancel")
static let pleaseEnterTheConferencePassword = localized("Please enter your room password")
}

View File

@@ -0,0 +1,224 @@
//
// ExitRoomView.swift
// TUIRoomKit
//
// Created by krabyu on 2023/8/23.
//
import Foundation
import RTCRoomEngine
class ExitRoomView: UIView {
private let viewModel: ExitRoomViewModel
private var isViewReady: Bool = false
var currentUser: UserEntity {
EngineManager.shared.store.currentUser
}
var roomInfo: TUIRoomInfo {
EngineManager.shared.store.roomInfo
}
let panelControl : UIControl = {
let control = UIControl()
control.backgroundColor = .clear
return control
}()
let contentView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = UIColor(0x17181F)
view.layer.cornerRadius = 12
return view
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0x7C85A6)
label.font = UIFont(name: "PingFangSC-Regular", size: 12)
label.textAlignment = .center
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.text = viewModel.isShownDestroyRoomButton() && viewModel.isShownLeaveRoomButton() ? .appointOwnerText : .leaveRoomTipText
return label
}()
let boundary1View: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x4F586B,alpha: 0.3)
return view
}()
let leaveRoomButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle(.leaveRoomText, for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 18)
button.setTitleColor(UIColor(0x006CFF), for: .normal)
button.backgroundColor = UIColor(0x17181F)
button.isEnabled = true
return button
}()
let boundary2View: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x4F586B,alpha: 0.3)
return view
}()
let destroyRoomButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle(.exitRoomText, for: .normal)
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 18)
button.setTitleColor(UIColor(0xE5395C), for: .normal)
button.backgroundColor = UIColor(0x17181F)
button.isEnabled = true
return button
}()
init(viewModel: ExitRoomViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
}
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
}
func constructViewHierarchy() {
addSubview(panelControl)
addSubview(contentView)
contentView.addSubview(titleLabel)
contentView.addSubview(boundary1View)
contentView.addSubview(leaveRoomButton)
contentView.addSubview(boundary2View)
contentView.addSubview(destroyRoomButton)
}
func activateConstraints() {
let titleLabelHeight = 67.scale375Height()
let leaveRoomButtonHeight = viewModel.isShownLeaveRoomButton() ? 57.scale375Height() : 0
let destroyRoomButtonHeight = currentUser.userId == roomInfo.ownerId ? 57.scale375Height() : 0
let space = 20.scale375Height()
let contentViewHeight = titleLabelHeight + leaveRoomButtonHeight + destroyRoomButtonHeight + space
panelControl.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
contentView.snp.makeConstraints { make in
make.height.equalTo(contentViewHeight)
make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
make.leading.equalTo(safeAreaLayoutGuide.snp.leading)
make.bottom.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(titleLabelHeight)
}
boundary1View.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom)
make.height.equalTo(1.scale375Height())
make.leading.trailing.equalToSuperview()
}
leaveRoomButton.snp.makeConstraints { make in
make.top.equalTo(boundary1View.snp.bottom)
make.height.equalTo(leaveRoomButtonHeight)
make.leading.trailing.equalToSuperview()
}
boundary2View.snp.makeConstraints { make in
make.top.equalTo(leaveRoomButton.snp.bottom)
make.height.equalTo(1.scale375Height())
make.leading.trailing.equalToSuperview()
}
destroyRoomButton.snp.makeConstraints { make in
make.top.equalTo(boundary2View.snp.bottom)
make.height.equalTo(destroyRoomButtonHeight)
make.leading.trailing.equalToSuperview()
}
}
func bindInteraction() {
setupViewState()
viewModel.viewResponder = self
leaveRoomButton.addTarget(self, action: #selector(leaveRoomAction), for: .touchUpInside)
destroyRoomButton.addTarget(self, action: #selector(destroyRoomAction), for: .touchUpInside)
contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
panelControl.addTarget(self, action: #selector(clickBackgroundView), for: .touchUpInside)
}
private func setupViewState() {
destroyRoomButton.isHidden = !viewModel.isShownDestroyRoomButton()
leaveRoomButton.isHidden = !viewModel.isShownLeaveRoomButton()
boundary2View.isHidden = !viewModel.isShownDestroyRoomButton() || !viewModel.isShownLeaveRoomButton()
}
@objc func clickBackgroundView() {
dismiss()
}
@objc func leaveRoomAction(sender: UIView) {
viewModel.leaveRoomAction()
}
@objc func destroyRoomAction(sender: UIView) {
viewModel.destroyRoom()
}
func show(rootView: UIView) {
rootView.addSubview(self)
self.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
UIView.animate(withDuration: 0.3) { [weak self] in
guard let self = self else { return }
self.alpha = 1
self.contentView.transform = .identity
}
}
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()
}
}
deinit {
debugPrint("deinit \(self)")
}
}
extension ExitRoomView: ExitRoomViewModelResponder {
func makeToast(message: String) {
makeToast(message)
}
func dismissView() {
dismiss()
}
}
private extension String {
static var leaveRoomTipText: String {
localized("Are you sure you want to leave the conference" )
}
static var appointOwnerText: String {
localized("If you do not want to end the conference, please appoint a new moderator before leaving the conference." )
}
static var leaveRoomText: String {
localized("Leave Conference")
}
static var exitRoomText: String {
localized("End Conference")
}
}

View File

@@ -0,0 +1,109 @@
//
// InviteView.swift
// TUIRoomKit
//
// Created by jeremiawang on 2024/8/26.
//
import Foundation
import Factory
class InviteView: UIView {
private var isViewReady: Bool = false
let stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.alignment = .center
view.distribution = .fillEqually
view.spacing = 0
return view
}()
private lazy var addUserView: ButtonItemView = {
let item = ButtonItemData()
item.normalTitle = .addUserText
item.normalIcon = "room_add_user"
item.resourceBundle = tuiRoomKitBundle()
item.buttonType = .muteAudioItemType
item.hasLineView = true
return ButtonItemView(itemData: item)
}()
private lazy var inviteToJoinView: ButtonItemView = {
let item = ButtonItemData()
item.normalTitle = .shareRoomText
item.normalIcon = "room_invite_to_join"
item.resourceBundle = tuiRoomKitBundle()
item.buttonType = .muteVideoItemType
item.hasLineView = true
return ButtonItemView(itemData: item)
}()
init() {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
self.layer.cornerRadius = 16
}
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
private func constructViewHierarchy() {
addSubview(stackView)
stackView.addArrangedSubview(addUserView)
stackView.addArrangedSubview(inviteToJoinView)
}
func activateConstraints() {
stackView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.bottom.equalToSuperview().offset(-34.scale375Height())
}
addUserView.snp.makeConstraints { make in
make.height.equalTo(53.scale375())
make.width.equalToSuperview()
}
inviteToJoinView.snp.makeConstraints { make in
make.height.equalTo(53.scale375())
make.width.equalToSuperview()
}
}
func bindInteraction() {
addUserView.itemData.action = { sender in
self.conferenceStore.dispatch(action: InvitationViewActions.showInvitationPopupView())
RoomRouter.shared.dismissPopupViewController()
}
inviteToJoinView.itemData.action = { sender in
RoomRouter.shared.presentPopUpViewController(viewType: .inviteMemberViewType, height: 290.scale375Height())
}
}
@Injected(\.navigation) private var route
@Injected(\.conferenceStore) var conferenceStore: ConferenceStore
}
private extension String {
static var addUserText: String {
localized("Add user")
}
static var shareRoomText: String {
localized("Share room")
}
}

View File

@@ -0,0 +1,160 @@
//
// MemberInviteView.swift
// TUIRoomKit
//
// Created by krabyu on 2023/8/21.
//
import Foundation
class MemberInviteView: UIView {
let viewModel: MemberInviteViewModel
private var isViewReady: Bool = false
var viewArray: [UIView] = []
let headView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x1B1E26)
return view
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = viewModel.title
label.textColor = UIColor(0xD5E0F2)
label.font = UIFont(name: "PingFangSC-Regular", size: 18)
label.textAlignment = .left
return label
}()
let stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.alignment = .center
view.distribution = .equalSpacing
view.spacing = 3
view.backgroundColor = UIColor(0x1B1E26)
return view
}()
let copyButton: UIButton = {
let button = UIButton()
button.setTitle(.copyRoomInformation, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
button.titleLabel?.textAlignment = .center
button.backgroundColor = UIColor(0x4F586B).withAlphaComponent(0.3)
button.layer.cornerRadius = 6
return button
}()
init(viewModel: MemberInviteViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
}
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 draw(_ rect: CGRect) {
super.draw(rect)
self.layer.cornerRadius = 12
}
func constructViewHierarchy() {
addSubview(stackView)
addSubview(headView)
headView.addSubview(titleLabel)
addSubview(copyButton)
}
func activateConstraints() {
headView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20.scale375())
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.height.equalTo(25.scale375())
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.equalToSuperview()
make.height.equalTo(25.scale375())
make.width.equalTo(182.scale375())
}
stackView.snp.makeConstraints { make in
make.top.equalTo(headView.snp.bottom).offset(20.scale375())
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
}
copyButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.height.equalTo(40.scale375Height())
make.top.equalTo(stackView.snp.bottom).offset(20.scale375Height())
}
for item in viewModel.messageItems {
let view = ListCellItemView(itemData: item)
viewArray.append(view)
stackView.addArrangedSubview(view)
view.snp.makeConstraints { make in
make.height.equalTo(24.scale375Height())
make.width.equalToSuperview()
}
}
}
func bindInteraction() {
backgroundColor = UIColor(0x1B1E26)
viewModel.viewResponder = self
copyButton.addTarget(self, action: #selector(copyAction(sender:)), for: .touchUpInside)
}
@objc func copyAction(sender: UIButton) {
viewModel.copyAction()
makeToast(.roomInformationCopiedSuccessfully)
}
deinit {
debugPrint("deinit \(self)")
}
}
extension MemberInviteView: MemberInviteResponder {
func showCopyToast(copyType: CopyType?) {
guard let copyType = copyType else { return }
var test: String
switch copyType {
case .copyRoomPassword:
test = .copyRoomPasswordSuccess
case .copyRoomIdType:
test = .copyRoomIdSuccess
case .copyRoomLinkType:
test = .copyRoomLinkSuccess
}
RoomRouter.makeToastInCenter(toast: test,duration: 0.5)
}
}
private extension String {
static var copyRoomIdSuccess: String {
localized("Conference ID copied.")
}
static var copyRoomLinkSuccess: String {
localized("Conference Link copied.")
}
static let conferencePasswordSuccess = localized("Conference password copied successfully.")
static let copyRoomInformation = localized("Copy room information")
static let roomInformationCopiedSuccessfully = localized("Room information copied successfully")
static let copyRoomPasswordSuccess = localized("Conference password copied")
}

View File

@@ -0,0 +1,86 @@
//
// RaiseHandNoticeView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/4/7.
//
import Foundation
class RaiseHandNoticeView: UIView {
let imageView: UIImageView = {
let image = UIImage(named: "room_raiseHand_notice", in: tuiRoomKitBundle(), compatibleWith: nil)
let imageView = UIImageView(image: image)
return imageView
}()
let dismissButton: UIButton = {
let button = UIButton()
let image = UIImage(named: "room_raiseHand_dismiss", in: tuiRoomKitBundle(), compatibleWith: nil)
button.setImage(image, for: .normal)
return button
}()
let label: UILabel = {
let label = UILabel()
label.text = .raiseHandNotice
label.textColor = .white
label.adjustsFontSizeToFitWidth = true
return label
}()
init() {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
debugPrint("deinit \(self)")
}
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
private func constructViewHierarchy() {
addSubview(imageView)
addSubview(label)
addSubview(dismissButton)
}
private func activateConstraints() {
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
label.snp.makeConstraints { make in
make.centerY.equalToSuperview().offset(-2)
make.leading.equalToSuperview().offset(12)
make.width.equalTo(250)
make.height.equalTo(20)
}
dismissButton.snp.makeConstraints { make in
make.centerY.equalToSuperview().offset(-2)
make.trailing.equalToSuperview().offset(-12)
make.width.height.equalTo(20)
}
}
private func bindInteraction() {
dismissButton.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
}
@objc func dismiss() {
isHidden = true
EngineManager.shared.changeRaiseHandNoticeState(isShown: false)
}
}
private extension String {
static var raiseHandNotice: String {
localized("Raise your hand to speak")
}
}

View File

@@ -0,0 +1,189 @@
//
// IntroduceRoomView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/3.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
class RoomInfoView: UIView {
let viewModel: RoomInfoViewModel
private var isViewReady: Bool = false
var viewArray: [UIView] = []
let headView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x1B1E26)
return view
}()
let nameLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xD5E0F2)
label.font = UIFont(name: "PingFangSC-Regular", size: 18)
label.textAlignment = isRTL ? .right : .left
return label
}()
let codeButton: UIButton = {
let button = UIButton()
let image = UIImage(named: "room_message_code", in: tuiRoomKitBundle(), compatibleWith: nil)
button.setImage(image, for: .normal)
button.isHidden = true
return button
}()
let stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.alignment = .center
view.distribution = .equalSpacing
view.spacing = 3
view.backgroundColor = UIColor(0x1B1E26)
return view
}()
let copyButton: UIButton = {
let button = UIButton()
button.setTitle(.copyRoomInformation, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
button.titleLabel?.textAlignment = .center
button.backgroundColor = UIColor(0x4F586B).withAlphaComponent(0.3)
button.layer.cornerRadius = 6
return button
}()
init(viewModel: RoomInfoViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToWindow() {
super.didMoveToWindow()
backgroundColor = UIColor(0x1B1E26)
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
override func draw(_ rect: CGRect) {
super.draw(rect)
self.layer.cornerRadius = 12
}
func constructViewHierarchy() {
addSubview(stackView)
addSubview(headView)
headView.addSubview(nameLabel)
headView.addSubview(codeButton)
addSubview(copyButton)
}
func activateConstraints() {
stackView.snp.makeConstraints { make in
make.top.equalTo(headView.snp.bottom).offset(20.scale375())
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
}
headView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20.scale375Height())
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.height.equalTo(25.scale375())
}
codeButton.snp.makeConstraints { make in
make.top.equalToSuperview()
make.trailing.equalToSuperview()
make.height.equalTo(25.scale375())
make.width.equalTo(68.scale375())
}
nameLabel.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.trailing.equalToSuperview()
make.height.equalTo(25.scale375())
}
copyButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.height.equalTo(40.scale375Height())
make.top.equalTo(stackView.snp.bottom).offset(20.scale375Height())
}
for item in viewModel.messageItems {
let view = ListCellItemView(itemData: item)
viewArray.append(view)
stackView.addArrangedSubview(view)
view.snp.makeConstraints { make in
make.height.equalTo(24.scale375Height())
make.width.equalToSuperview()
}
}
}
func bindInteraction() {
backgroundColor = UIColor(0x1B1E26)
setupViewState(item: viewModel)
codeButton.addTarget(self, action: #selector(codeAction(sender:)), for: .touchUpInside)
copyButton.addTarget(self, action: #selector(copyAction(sender: )), for: .touchUpInside)
viewModel.viewResponder = self
}
func setupViewState(item: RoomInfoViewModel) {
nameLabel.text = viewModel.title
}
@objc func codeAction(sender: UIButton) {
viewModel.codeAction(sender: sender)
}
@objc func copyAction(sender: UIButton) {
viewModel.copyConferenceDetails()
makeToast(.roomInformationCopiedSuccessfully)
}
deinit {
debugPrint("deinit \(self)")
}
}
extension RoomInfoView: RoomInfoResponder {
func updateNameLabel(_ text: String) {
nameLabel.text = text
}
func showCopyToast(copyType: CopyType?) {
var test: String
guard let copyType = copyType else { return }
switch copyType {
case .copyRoomPassword:
test = .copyRoomPasswordSuccess
case .copyRoomIdType:
test = .copyRoomIdSuccess
case .copyRoomLinkType:
test = .copyRoomLinkSuccess
}
RoomRouter.makeToastInCenter(toast: test,duration: 0.5)
}
}
private extension String {
static var copyRoomIdSuccess: String {
localized("Conference ID copied.")
}
static var copyRoomLinkSuccess: String {
localized("Conference Link copied.")
}
static let conferencePasswordSuccess = localized("Conference password copied successfully.")
static let copyRoomInformation = localized("Copy room information")
static let roomInformationCopiedSuccessfully = localized("Room information copied successfully")
static let copyRoomPasswordSuccess = localized("Conference password copied")
}

View File

@@ -0,0 +1,97 @@
//
// RoomUserStatusView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/7/24.
//
import Foundation
class RoomUserStatusView: UIView {
private var isOwner: Bool = false
private var isViewReady: Bool = false
private let homeOwnerImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil))
imageView.layer.cornerRadius = 12
imageView.layer.masksToBounds = true
return imageView
}()
private let userNameLabel: UILabel = {
let user = UILabel()
user.textColor = .white
user.backgroundColor = UIColor.clear
user.textAlignment = isRTL ? .right : .left
user.numberOfLines = 1
user.font = UIFont(name: "PingFangSC-Regular", size: 12)
return user
}()
private let voiceVolumeImageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
isViewReady = true
constructViewHierarchy()
activateConstraints()
backgroundColor = UIColor(0x22262E, alpha: 0.8)
layer.cornerRadius = 12
layer.masksToBounds = true
}
private func constructViewHierarchy() {
addSubview(homeOwnerImageView)
addSubview(voiceVolumeImageView)
addSubview(userNameLabel)
}
private func activateConstraints() {
updateViewConstraints()
userNameLabel.snp.makeConstraints { make in
make.leading.equalTo(voiceVolumeImageView.snp.trailing).offset(5)
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().offset(-8)
}
voiceVolumeImageView.snp.makeConstraints { make in
make.leading.equalTo(homeOwnerImageView.snp.trailing).offset(6.scale375())
make.width.height.equalTo(14)
make.centerY.equalToSuperview()
}
}
private func updateViewConstraints() {
guard homeOwnerImageView.superview != nil else { return }
homeOwnerImageView.snp.remakeConstraints { make in
make.leading.equalToSuperview()
make.height.equalTo(24)
make.top.bottom.equalToSuperview()
make.width.equalTo(isOwner ? 24 : 0)
}
}
}
extension RoomUserStatusView {
func updateUserStatus(userModel: UserEntity) {
if !userModel.userName.isEmpty {
userNameLabel.text = userModel.userName
} else {
userNameLabel.text = userModel.userId
}
isOwner = userModel.userId == EngineManager.shared.store.roomInfo.ownerId
updateViewConstraints()
updateUserVolume(hasAudio: userModel.hasAudioStream, volume: userModel.userVoiceVolume)
}
func updateUserVolume(hasAudio: Bool, volume: Int) {
if !hasAudio {
voiceVolumeImageView.image = UIImage(named: "room_mute_audio", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn()
} else {
let volumeImageName = volume <= 0 ? "room_voice_volume1" : "room_voice_volume2"
voiceVolumeImageView.image = UIImage(named: volumeImageName, in: tuiRoomKitBundle(), compatibleWith: nil)
}
}
}

View File

@@ -0,0 +1,225 @@
//
// RoomVideoFloatView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/7/11.
//
import Foundation
import Factory
class RoomVideoFloatView: UIView {
@Injected(\.floatChatService) private var store: FloatChatStoreProvider
@Injected(\.conferenceStore) private var conferenceStore: ConferenceStore
private var isDraging: Bool = false
private let viewModel: RoomVideoFloatViewModel
private let space: CGFloat = 10
private let renderView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x5C5C5C)
return view
}()
private let shutterView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x17181F)
view.isHidden = true
return view
}()
private let avatarImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.layer.masksToBounds = true
imageView.isHidden = true
return imageView
}()
private let userStatusView: RoomUserStatusView = {
let view = RoomUserStatusView(frame: .zero)
return view
}()
override init(frame: CGRect) {
viewModel = RoomVideoFloatViewModel()
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
NSObject.cancelPreviousPerformRequests(withTarget: self)
debugPrint("deinit:\(self)")
}
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
backgroundColor = .clear
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
override func draw(_ rect: CGRect) {
super.draw(rect)
roundedRect(rect: bounds,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: 10, height: 10))
avatarImageView.roundedCircle(rect: avatarImageView.bounds)
}
override func layoutSubviews() {
super.layoutSubviews()
if !isDraging {
self.center = adsorption(centerPoint: self.center)
}
}
func constructViewHierarchy() {
addSubview(renderView)
addSubview(shutterView)
addSubview(avatarImageView)
addSubview(userStatusView)
}
func activateConstraints() {
renderView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
shutterView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
avatarImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.height.width.equalTo(50)
}
userStatusView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(5)
make.bottom.equalToSuperview().offset(-5)
make.width.lessThanOrEqualTo(self).multipliedBy(0.9)
}
}
func bindInteraction() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(panGesture:)))
addGestureRecognizer(panGesture)
let tap = UITapGestureRecognizer(target: self, action: #selector(didTap(sender:)))
addGestureRecognizer(tap)
viewModel.viewResponder = self
viewModel.showFloatWindowViewVideo(renderView: renderView)
setupViewState()
}
private func setupViewState() {
guard let userModel = viewModel.engineManager.store.attendeeList.first(where: { $0.userId == viewModel.userId }) else { return }
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
avatarImageView.sd_setImage(with: URL(string: userModel.avatarUrl), placeholderImage: placeholder)
userStatusView.updateUserVolume(hasAudio: userModel.hasAudioStream, volume: userModel.userVoiceVolume)
}
@objc func didTap(sender: UIView) {
viewModel.showRoomMainView()
}
@objc func didPan(panGesture: UIPanGestureRecognizer) {
guard let viewSuperview = superview else { return }
let moveState = panGesture.state
let viewCenter = center
switch moveState {
case .changed:
isDraging = true
let point = panGesture.translation(in: viewSuperview)
center = CGPoint(x: viewCenter.x + point.x, y: viewCenter.y + point.y)
break
case .ended:
let point = panGesture.translation(in: viewSuperview)
let newPoint = CGPoint(x: viewCenter.x + point.x, y: viewCenter.y + point.y)
UIView.animate(withDuration: 0.2) {
self.center = self.adsorption(centerPoint: newPoint)
}
isDraging = false
break
default: break
}
panGesture.setTranslation(.zero, in: viewSuperview)
}
class func show(width: CGFloat = 100, height: CGFloat = 180) {
DispatchQueue.main.async {
guard let currentWindow = RoomRouter.getCurrentWindow() else { return }
let roomFloatView = RoomVideoFloatView()
currentWindow.addSubview(roomFloatView)
roomFloatView.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-5)
make.bottom.equalToSuperview().offset(-100)
make.width.equalTo(width)
make.height.equalTo(height)
}
}
}
class func dismiss() {
DispatchQueue.main.async {
guard let currentWindow = RoomRouter.getCurrentWindow() else { return }
for view in currentWindow.subviews where view is RoomVideoFloatView {
view.removeFromSuperview()
}
}
}
private func adsorption(centerPoint: CGPoint) -> CGPoint {
guard let viewSuperview = superview else { return centerPoint }
let limitMargin = 5.0
let frame = self.frame
let point = CGPoint(x: centerPoint.x - frame.width / 2, y: centerPoint.y - frame.height / 2)
var newPoint = point
if centerPoint.x < (viewSuperview.frame.width / 2) {
newPoint.x = limitMargin
} else {
newPoint.x = viewSuperview.frame.width - frame.width - limitMargin
}
if point.y <= limitMargin {
newPoint.y = limitMargin
} else if (point.y + frame.height) > (viewSuperview.frame.height - limitMargin) {
newPoint.y = viewSuperview.frame.height - frame.height - limitMargin
}
return CGPoint(x: newPoint.x + frame.width / 2, y: newPoint.y + frame.height / 2)
}
private func resetVolume() {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(resetVolumeView), object: nil)
perform(#selector(resetVolumeView), with: nil, afterDelay: 1)
}
@objc func resetVolumeView() {
guard let userItem = viewModel.getUserEntity(userId: viewModel.userId) else { return }
userStatusView.updateUserVolume(hasAudio: userItem.hasAudioStream, volume: 0)
}
}
extension RoomVideoFloatView: RoomVideoFloatViewResponder {
func makeToast(text: String) {
RoomRouter.makeToastInCenter(toast: text, duration: 0.5)
}
func updateUserStatus(user: UserEntity) {
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
avatarImageView.sd_setImage(with: URL(string: user.avatarUrl), placeholderImage: placeholder)
userStatusView.updateUserStatus(userModel: user)
}
func updateUserAudioVolume(hasAudio: Bool, volume: Int) {
userStatusView.updateUserVolume(hasAudio: hasAudio, volume: volume)
resetVolume()
}
func showAvatarImageView(isShow: Bool) {
shutterView.isHidden = !isShow
avatarImageView.isHidden = !isShow
}
}

View File

@@ -0,0 +1,86 @@
//
// LocalAudioView.swift
// TUIRoomKit
//
// Created by janejntang on 2024/1/5.
//
import Foundation
class LocalAudioView: UIView {
let viewModel: LocalAudioViewModel
lazy var muteAudioButton : UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "room_mic_on", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
button.setImage(UIImage(named: "room_mic_off", in: tuiRoomKitBundle(), compatibleWith: nil), for: .selected)
button.isSelected = viewModel.checkMuteAudioSelectedState()
button.backgroundColor = UIColor(0x2A2D38)
button.layer.cornerRadius = 12
return button
}()
init(viewModel: LocalAudioViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
self.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
debugPrint("deinit:\(self)")
}
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
private func constructViewHierarchy() {
addSubview(muteAudioButton)
}
private func activateConstraints() {
muteAudioButton.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private func bindInteraction() {
viewModel.viewResponder = self
muteAudioButton.addTarget(self, action: #selector(muteAudioAction(sender:)), for: .touchUpInside)
}
@objc func muteAudioAction(sender: UIButton) {
viewModel.muteAudioAction()
}
func show() {
UIView.animate(withDuration: 0.3) { [weak self] () in
guard let self = self else { return }
self.transform = .identity
} completion: { _ in
}
}
func hide() {
self.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
}
}
extension LocalAudioView: LocalAudioViewModelResponder {
func updateMuteAudioButton(isSelected: Bool) {
muteAudioButton.isSelected = isSelected
}
func makeToast(text: String) {
RoomRouter.makeToastInCenter(toast: text, duration: 1)
}
}

View File

@@ -0,0 +1,231 @@
//
// MediaSettingView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/16.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
class MediaSettingView: UIView {
let viewModel: MediaSettingViewModel
lazy var setUpTableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = UIColor(0x1B1E26)
tableView.register(UserListCell.self, forCellReuseIdentifier: "MediaSettingViewCell")
tableView.sectionHeaderHeight = 48.scale375()
return tableView
}()
init(viewModel: MediaSettingViewModel) {
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(0x1B1E26)
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
private func constructViewHierarchy() {
addSubview(setUpTableView)
}
private func activateConstraints() {
setUpTableView.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.leading.equalToSuperview().offset(13.scale375())
make.trailing.equalToSuperview().offset(-13.scale375())
}
}
private func bindInteraction() {
viewModel.viewResponder = self
}
deinit {
debugPrint("deinit \(self)")
}
}
extension MediaSettingView: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return viewModel.videoItems.count
} else if section == 1 {
return viewModel.audioItems.count
} else if section == 2 {
return viewModel.otherItems.count
} else {
return 0
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.topItems.count
}
}
extension MediaSettingView: UITableViewDelegate {
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var itemData = ListCellItemData()
if indexPath.section == 0, indexPath.row < viewModel.videoItems.count {
itemData = viewModel.videoItems[indexPath.row]
} else if indexPath.section == 1 {
itemData = viewModel.audioItems[indexPath.row]
} else if indexPath.section == 2 {
itemData = viewModel.otherItems[indexPath.row]
}
let cell = MediaSettingViewCell(itemData: itemData)
cell.selectionStyle = .none
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 55.scale375()
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerLabel = UILabel()
headerLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular)
headerLabel.textColor = UIColor(0xD8D8D8)
headerLabel.textAlignment = isRTL ? .right : .left
headerLabel.text = viewModel.topItems[safe: section]
return headerLabel
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let rows = tableView.numberOfRows(inSection: indexPath.section)
if indexPath.row == 0 || indexPath.row == rows - 1 {
var corner = UIRectCorner()
if rows == 1 {
corner = .allCorners
} else if indexPath.row == 0 {
corner = [.topLeft, .topRight]
} else if indexPath.row == rows - 1 {
corner = [.bottomLeft, .bottomRight]
}
cell.roundedRect(rect: cell.bounds,
byRoundingCorners: corner,
cornerRadii: CGSize(width: 12, height: 12))
}
}
}
extension MediaSettingView: MediaSettingViewEventResponder {
func showFrameRateAlert() {
let frameRateAlert = VideoChoicePanel()
frameRateAlert.titleText = .frameRateText
frameRateAlert.dataSource = viewModel.frameRateArray
frameRateAlert.selectIndex = viewModel.getCurrentFrameRateIndex()
frameRateAlert.didSelectItem = { [weak self] index in
guard let `self` = self else { return }
self.viewModel.changeFrameRateAction(index: index)
}
frameRateAlert.show(rootView: self)
}
func showResolutionAlert() {
let resolutionAlert = VideoChoicePanel()
resolutionAlert.titleText = .resolutionText
resolutionAlert.dataSource = viewModel.resolutionNameItems
resolutionAlert.selectIndex = viewModel.getCurrentResolutionIndex()
resolutionAlert.didSelectItem = { [weak self] index in
guard let `self` = self else { return }
self.viewModel.changeResolutionAction(index: index)
}
resolutionAlert.show(rootView: self)
}
func showQualityView() {
let qualityInfoPanel = QualityInfoPanel()
qualityInfoPanel.show(rootView: self)
}
func updateStackView(item: ListCellItemData) {
for view in setUpTableView.visibleCells where view is MediaSettingViewCell {
guard let cell = view as? MediaSettingViewCell else { continue }
guard cell.itemData.type == item.type else { continue }
cell.updateStackView(item: item)
}
}
func makeToast(text: String) {
RoomRouter.makeToastInCenter(toast: text, duration: 1)
}
}
class MediaSettingViewCell: UITableViewCell {
var itemData: ListCellItemData
lazy var listCell: ListCellItemView = {
let view = ListCellItemView(itemData: itemData)
return view
}()
init(itemData: ListCellItemData) {
self.itemData = itemData
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")
}
private func constructViewHierarchy() {
contentView.addSubview(listCell)
}
private func activateConstraints() {
listCell.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(13.scale375())
make.trailing.equalToSuperview().offset(-13.scale375())
make.top.bottom.equalToSuperview()
}
}
private func bindInteraction() {
backgroundColor = UIColor(0x242934)
}
func updateStackView(item: ListCellItemData) {
listCell.setupViewState(item: item)
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var resolutionText: String {
localized("Resolution")
}
static var frameRateText: String {
localized("Frame Rate")
}
}

View File

@@ -0,0 +1,379 @@
//
// QualityInfoPanel.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/4/19.
// Copyright © 2024 Tencent. All rights reserved.
//
import Foundation
class QualityInfoPanel: UIView {
var viewModel: QualityInfoViewModel = QualityInfoViewModel()
let landscapeHight: CGFloat = min(kScreenWidth, kScreenHeight)
let portraitHight: CGFloat = 411.scale375Height()
private let arrowViewHeight: CGFloat = 35.0
private let cellHeight: CGFloat = 33.0
private let lineViewHorizontalMargin = 16.0
private let headerHeight = 48
private var currentLandscape: Bool = isLandscape
let bgView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .black
view.alpha = 0.6
return view
}()
let contentView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = UIColor(0x22262E)
view.layer.cornerRadius = 12
return view
}()
private let dropArrowView : UIView = {
let view = UIView()
return view
}()
private let dropArrowImageView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "room_drop_arrow", in:tuiRoomKitBundle(), compatibleWith: nil)
return view
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.separatorStyle = .none
tableView.backgroundColor = UIColor(0x22262E)
tableView.delegate = self
tableView.dataSource = self
tableView.register(QualityTableViewCell.self,
forCellReuseIdentifier: "QualityTableViewCell")
tableView.sectionHeaderHeight = headerHeight.scale375()
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = 0
}
return tableView
}()
var willDismiss: (() -> Void)?
var didDismiss: (() -> Void)?
override init(frame: CGRect = .zero) {
super.init(frame: frame)
contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
alpha = 0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
self.viewModel.viewResponder = self
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
func show(rootView: UIView) {
rootView.addSubview(self)
self.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
UIView.animate(withDuration: 0.3) { [weak self] in
guard let self = self else { return }
self.alpha = 1
self.contentView.transform = .identity
}
}
func dismiss() {
if let action = willDismiss {
action()
}
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 }
if let action = self.didDismiss {
action()
}
self.removeFromSuperview()
}
}
override func layoutSubviews() {
super.layoutSubviews()
guard currentLandscape != isLandscape else { return }
setupViewOrientation(isLandscape: isLandscape)
currentLandscape = isLandscape
}
func constructViewHierarchy() {
addSubview(bgView)
addSubview(contentView)
dropArrowView.addSubview(dropArrowImageView)
contentView.addSubview(dropArrowView)
contentView.addSubview(tableView)
}
func activateConstraints() {
bgView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
setupViewOrientation(isLandscape: isLandscape)
dropArrowView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(arrowViewHeight)
}
dropArrowImageView.snp.makeConstraints { make in
make.centerX.centerY.equalToSuperview()
make.width.equalTo(24.scale375())
make.height.equalTo(3.scale375())
}
tableView.snp.makeConstraints { make in
make.top.equalTo(dropArrowView.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
}
}
func bindInteraction() {
let dropArrowTap = UITapGestureRecognizer(target: self, action: #selector(dropDownPopUpViewAction(sender:)))
dropArrowView.addGestureRecognizer(dropArrowTap)
dropArrowView.isUserInteractionEnabled = true
}
private func setupViewOrientation(isLandscape: Bool) {
contentView.snp.remakeConstraints { make in
if isLandscape {
make.height.equalTo(landscapeHight)
} else {
make.height.equalTo(portraitHight)
}
make.leading.trailing.bottom.equalToSuperview()
}
}
@objc func dropDownPopUpViewAction(sender: UIView) {
dismiss()
}
deinit {
debugPrint("deinit \(self)")
}
}
extension QualityInfoPanel: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.sections[section].items.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.sections.count
}
}
extension QualityInfoPanel: UITableViewDelegate {
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "QualityTableViewCell", for: indexPath)
if let qualityCell = cell as? QualityTableViewCell {
qualityCell.setCellModel(model: self.viewModel.sections[indexPath.section].items[indexPath.row])
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeight.scale375()
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
let headerLabel = UILabel()
headerLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular)
headerLabel.textColor = UIColor(0x99A2B2)
headerLabel.textAlignment = isRTL ? .right : .left
headerLabel.text = viewModel.sections[safe: section]?.titleText
headerView.addSubview(headerLabel)
headerLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
}
return headerView
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
if section == self.viewModel.sections.count - 1 {
return 0
}
return 0.5
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
if section == self.viewModel.sections.count - 1 {
return nil
}
let footerView = UIView()
let lineView = UIView(frame: CGRect(x: lineViewHorizontalMargin,
y: 0,
width: tableView.frame.size.width - 2 * lineViewHorizontalMargin,
height: 0.5))
lineView.backgroundColor = UIColor(0xB2BBD1)
footerView.addSubview(lineView)
return footerView
}
}
extension QualityInfoPanel: QualityViewResponder {
func reloadData() {
self.tableView.reloadData()
}
}
class QualityTableViewCell: UITableViewCell {
let titleLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
label.textColor = UIColor(0xE7ECF6)
label.sizeToFit()
return label
}()
let normalInfoLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
label.textColor = UIColor(0xE7ECF6)
label.isHidden = true
label.sizeToFit()
return label
}()
let upInfoLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
label.textColor = UIColor(0xE7ECF6)
label.isHidden = true
label.sizeToFit()
return label
}()
let uplinkImageView: UIImageView = {
let norImage = UIImage(named: "room_uplink_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
let imageView = UIImageView(image: norImage)
imageView.isHidden = true
return imageView
}()
let downInfoLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
label.textColor = UIColor(0xE7ECF6)
label.isHidden = true
label.sizeToFit()
return label
}()
let downlinkImageView: UIImageView = {
let norImage = UIImage(named: "room_downlink_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
let imageView = UIImageView(image: norImage)
imageView.isHidden = true
return imageView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
backgroundColor = .clear
selectionStyle = .none
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
}
private func constructViewHierarchy() {
contentView.addSubview(titleLabel)
contentView.addSubview(normalInfoLabel)
contentView.addSubview(downInfoLabel)
contentView.addSubview(downlinkImageView)
contentView.addSubview(upInfoLabel)
contentView.addSubview(uplinkImageView)
}
private func activateConstraints() {
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalToSuperview().offset(16)
}
normalInfoLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().offset(-16)
}
downlinkImageView.snp.makeConstraints { make in
make.width.height.equalTo(16)
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().offset(-16)
}
downInfoLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalTo(downlinkImageView.snp.leading).offset(-4)
}
uplinkImageView.snp.makeConstraints { make in
make.width.height.equalTo(16)
make.centerY.equalToSuperview()
make.trailing.equalTo(downInfoLabel.snp.leading).offset(-16)
}
upInfoLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalTo(uplinkImageView.snp.leading).offset(-4)
}
}
func setCellModel(model: QualityCellModel) {
self.titleLabel.text = model.titleText
if model.type == .upDown {
showUpDownInfo(isShow: true)
showNormalInfo(isShow: false)
upInfoLabel.text = model.uplinkString
downInfoLabel.text = model.downlinkString
} else {
showUpDownInfo(isShow: false)
showNormalInfo(isShow: true)
normalInfoLabel.text = model.normalString
}
}
private func showUpDownInfo(isShow: Bool) {
upInfoLabel.isHidden = !isShow
uplinkImageView.isHidden = !isShow
downInfoLabel.isHidden = !isShow
downlinkImageView.isHidden = !isShow
}
private func showNormalInfo(isShow: Bool) {
normalInfoLabel.isHidden = !isShow
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@@ -0,0 +1,304 @@
//
// VideoChoicePanel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/17.
// Copyright © 2023 Tencent. All rights reserved.
// Video resolution or frame rate selection panel
//
import Foundation
// MARK: Resolution
class VideoChoicePanel: AlertContentView {
var dataSource: [String] = []
var selectIndex = 3
var titleText: String = ""
var didSelectItem: ((_ index: Int) -> Void)?
let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.separatorStyle = .none
tableView.backgroundColor = UIColor(0x22262E)
return tableView
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func constructViewHierarchy() {
super.constructViewHierarchy()
contentView.addSubview(tableView)
}
override func activateConstraints() {
super.activateConstraints()
tableView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(space.scale375Height())
make.leading.trailing.bottom.equalToSuperview()
}
}
override func bindInteraction() {
super.bindInteraction()
titleLabel.text = titleText
tableView.dataSource = self
tableView.delegate = self
tableView.register(ResolutionTableViewCell.self,
forCellReuseIdentifier: "ResolutionTableViewCell")
}
deinit {
debugPrint("deinit \(self)")
}
}
extension VideoChoicePanel: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ResolutionTableViewCell", for: indexPath)
if let scell = cell as? ResolutionTableViewCell {
scell.titleLabel.text = dataSource[indexPath.row]
scell.isSelected = indexPath.row == selectIndex
}
return cell
}
}
extension VideoChoicePanel: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectIndex = indexPath.row
tableView.reloadSections(IndexSet(integer: 0), with: .none)
if let action = didSelectItem {
action(selectIndex)
}
dismiss()
}
}
class ResolutionTableViewCell: UITableViewCell {
let titleLabel: UILabel = {
let label = UILabel(frame: .zero)
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
label.textColor = UIColor(0xD1D9EC)
return label
}()
let checkboxImageView: UIImageView = {
let norImage = UIImage(named: "room_checkbox_sel", in: tuiRoomKitBundle(), compatibleWith: nil)
let imageView = UIImageView(image: norImage)
imageView.isHidden = true
return imageView
}()
override var isSelected: Bool {
didSet {
checkboxImageView.isHidden = !isSelected
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
backgroundColor = .clear
selectionStyle = .none
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
}
func constructViewHierarchy() {
contentView.addSubview(titleLabel)
contentView.addSubview(checkboxImageView)
}
func activateConstraints() {
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalToSuperview().offset(20)
}
checkboxImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().offset(-20)
make.size.equalTo(CGSize(width: 16, height: 16))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
debugPrint("deinit \(self)")
}
}
// MARK: Base
class AlertContentView: UIView {
let space: Int = 16
let landscapeHight: CGFloat = min(kScreenWidth, kScreenHeight)
let portraitHight: CGFloat = 718.scale375Height()
private var currentLandscape: Bool = isLandscape
let bgView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .black
view.alpha = 0.6
return view
}()
let contentView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = UIColor(0x22262E)
view.layer.cornerRadius = 12
return view
}()
let backButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "room_back_white", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
button.backgroundColor = .clear
return button
}()
let titleLabel: UILabel = {
let label = UILabel(frame: .zero)
label.textColor = UIColor(0xD1D9EC)
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
label.font = UIFont(name: "PingFangSC-Medium", size: 16)
return label
}()
var willDismiss: (() -> Void)?
var didDismiss: (() -> Void)?
override init(frame: CGRect = .zero) {
super.init(frame: frame)
contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
alpha = 0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
func show(rootView: UIView) {
rootView.addSubview(self)
self.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
UIView.animate(withDuration: 0.3) { [weak self] in
guard let self = self else { return }
self.alpha = 1
self.contentView.transform = .identity
}
}
func dismiss() {
if let action = willDismiss {
action()
}
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 }
if let action = self.didDismiss {
action()
}
self.removeFromSuperview()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self) else {
return
}
let backButtonFrame = backButton.frame.inset(by: UIEdgeInsets.init(top: -space.scale375Height(), left: -space.scale375(), bottom: -space.scale375Height(), right: -space.scale375()))
if !contentView.frame.contains(point) || backButtonFrame.contains(point) {
dismiss()
}
}
override func layoutSubviews() {
super.layoutSubviews()
guard currentLandscape != isLandscape else { return }
setupViewOrientation(isLandscape: isLandscape)
currentLandscape = isLandscape
}
func constructViewHierarchy() {
addSubview(bgView)
addSubview(contentView)
contentView.addSubview(backButton)
contentView.addSubview(titleLabel)
}
func activateConstraints() {
bgView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
setupViewOrientation(isLandscape: isLandscape)
backButton.snp.makeConstraints { make in
make.width.height.equalTo(16)
make.leading.equalToSuperview().offset(space.scale375())
make.centerY.equalTo(titleLabel)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(space.scale375Height())
make.centerX.equalToSuperview()
}
}
func bindInteraction() {
backButton.addTarget(self, action: #selector(backAction(sender:)), for: .touchUpInside)
}
private func setupViewOrientation(isLandscape: Bool) {
contentView.snp.remakeConstraints { make in
if isLandscape {
make.height.equalTo(landscapeHight)
} else {
make.height.equalTo(portraitHight)
}
make.leading.trailing.bottom.equalToSuperview()
}
}
@objc func backAction(sender: UIButton) {
dismiss()
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,207 @@
//
// PopUpViewController.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/12.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import TUICore
protocol PopUpViewResponder: AnyObject {
func updateAlertTransitionPosition(position: AlertTransitionAnimator.AlertTransitionPosition)
}
class PopUpView: UIView {
let viewModel: PopUpViewModel
var rootView: UIView?
weak var responder: PopUpViewResponder?
private let arrowViewHeight: CGFloat = 35.0
private var currentLandscape: Bool = isLandscape
private let panelControl : UIControl = {
let control = UIControl()
control.backgroundColor = .clear
return control
}()
private let dropArrowView : UIView = {
let view = UIView()
return view
}()
private let dropArrowImageView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "room_drop_arrow",in:tuiRoomKitBundle(),compatibleWith: nil)
return view
}()
private let rightArrowView : UIView = {
let view = UIView()
return view
}()
private let rightArrowImageView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "room_right_arrow",in:tuiRoomKitBundle(),compatibleWith: nil)
return view
}()
private let backgroundView: UIView = {
let view = UIView()
return view
}()
init(viewModel: PopUpViewModel) {
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
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
func constructViewHierarchy() {
addSubview(panelControl)
addSubview(backgroundView)
backgroundView.addSubview(dropArrowView)
backgroundView.addSubview(rightArrowView)
dropArrowView.addSubview(dropArrowImageView)
rightArrowView.addSubview(rightArrowImageView)
setupViewState()
guard let rootView = rootView else { return }
backgroundView.addSubview(rootView)
backgroundView.layer.cornerRadius = 15
}
func activateConstraints() {
panelControl.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
setupViewOrientation(isLandscape: isLandscape)
dropArrowView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
make.height.equalTo(arrowViewHeight)
}
dropArrowImageView.snp.makeConstraints { make in
make.centerX.centerY.equalToSuperview()
make.width.equalTo(24.scale375())
make.height.equalTo(3.scale375())
}
rightArrowView.snp.makeConstraints { make in
make.top.leading.bottom.equalToSuperview()
make.width.equalTo(arrowViewHeight)
}
rightArrowImageView.snp.makeConstraints { make in
make.centerY.centerX.equalToSuperview()
make.width.equalTo(3.scale375())
make.height.equalTo(24.scale375())
}
}
func bindInteraction() {
backgroundView.backgroundColor = viewModel.backgroundColor
let dropArrowTap = UITapGestureRecognizer(target: self, action: #selector(dropDownPopUpViewAction(sender:)))
dropArrowView.addGestureRecognizer(dropArrowTap)
dropArrowView.isUserInteractionEnabled = true
let rightArrowTap = UITapGestureRecognizer(target: self, action: #selector(dropDownPopUpViewAction(sender:)))
rightArrowView.addGestureRecognizer(rightArrowTap)
rightArrowView.isUserInteractionEnabled = true
panelControl.addTarget(self, action: #selector(panelControlAction), for: .touchUpInside)
}
func setupViewState() {
switch viewModel.viewType {
case .roomInfoViewType:
let model = RoomInfoViewModel()
rootView = RoomInfoView(viewModel: model)
case .mediaSettingViewType:
let model = MediaSettingViewModel()
let view = MediaSettingView(viewModel: model)
rootView = view
case .userListViewType:
let model = UserListViewModel()
rootView = UserListView(viewModel: model)
case .raiseHandApplicationListViewType:
let model = RaiseHandApplicationListViewModel()
rootView = RaiseHandApplicationListView(viewModel: model)
case .transferMasterViewType:
let model = TransferMasterViewModel()
viewModel.viewResponder = model
rootView = TransferMasterView(viewModel: model)
case .QRCodeViewType:
let model = QRCodeViewModel(urlString: "https://web.sdk.qcloud.com/component/tuiroom/index.html#/" + "#/room?roomId=" +
EngineManager.shared.store.roomInfo.roomId)
rootView = QRCodeView(viewModel: model)
case .inviteViewType:
rootView = InviteView()
case .inviteMemberViewType:
let model = MemberInviteViewModel()
rootView = MemberInviteView(viewModel: model)
default: break
}
}
@objc func dropDownPopUpViewAction(sender: UIView) {
RoomRouter.shared.dismissPopupViewController()
}
override func layoutSubviews() {
super.layoutSubviews()
guard currentLandscape != isLandscape else { return }
setupViewOrientation(isLandscape: isLandscape)
responder?.updateAlertTransitionPosition(position: isLandscape ? .right : .bottom)
currentLandscape = isLandscape
}
func setupViewOrientation(isLandscape: Bool) {
let width = min(kScreenHeight, kScreenWidth)
let height = max(kScreenHeight, kScreenWidth)
if isLandscape {
backgroundView.snp.remakeConstraints { make in
make.width.equalTo(width + arrowViewHeight)
make.top.equalToSuperview()
make.height.equalToSuperview()
make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing)
}
} else {
let currentHeight = min(viewModel.height + arrowViewHeight, height - arrowViewHeight)
backgroundView.snp.remakeConstraints { make in
make.width.bottom.equalToSuperview()
make.height.equalTo(currentHeight)
}
}
rootView?.snp.remakeConstraints { make in
if isLandscape {
make.leading.equalToSuperview().offset(arrowViewHeight)
make.trailing.top.bottom.equalToSuperview()
} else {
let currentHeight = min(viewModel.height, height - 2*arrowViewHeight)
make.height.equalTo(currentHeight)
make.trailing.leading.bottom.equalToSuperview()
}
}
rightArrowView.isHidden = !isLandscape
dropArrowView.isHidden = isLandscape
}
@objc func panelControlAction() {
viewModel.panelControlAction()
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,80 @@
//
// PopUpViewController.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/12.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import UIKit
protocol PopUpViewModelFactory {
func makeRootViewModel(viewType: PopUpViewType, height:CGFloat, backgroundColor: UIColor) -> PopUpViewModel
}
class PopUpViewController: UIViewController {
let viewModel: PopUpViewModel
var duration = 0.5
var alertTransitionStyle: AlertTransitionAnimator.AlertTransitionStyle = .present
var alertTransitionPosition: AlertTransitionAnimator.AlertTransitionPosition = .bottom
var transitionAnimator: AlertTransitionAnimator?
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .allButUpsideDown
}
init(popUpViewModelFactory: PopUpViewModelFactory, viewType: PopUpViewType, height: CGFloat, backgroundColor: UIColor) {
viewModel = popUpViewModelFactory.makeRootViewModel(viewType: viewType, height: height, backgroundColor: backgroundColor)
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
transitioningDelegate = self
if isLandscape {
self.alertTransitionPosition = .right
} else {
self.alertTransitionPosition = .bottom
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let rootView = PopUpView(viewModel: viewModel)
rootView.responder = self
view = rootView
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: false)
UIApplication.shared.isIdleTimerDisabled = true
}
deinit {
debugPrint("deinit \(self)")
}
}
extension PopUpViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) ->
UIViewControllerAnimatedTransitioning? {
transitionAnimator = AlertTransitionAnimator()
transitionAnimator?.alertTransitionStyle = .present
transitionAnimator?.alertTransitionPosition = alertTransitionPosition
transitionAnimator?.duration = duration
return transitionAnimator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator?.alertTransitionStyle = .dismiss
return transitionAnimator
}
}
extension PopUpViewController: PopUpViewResponder {
func updateAlertTransitionPosition(position: AlertTransitionAnimator.AlertTransitionPosition) {
transitionAnimator?.alertTransitionPosition = position
}
}

View File

@@ -0,0 +1,175 @@
//
// RaiseHandApplicationCell.swift
// TUIRoomKit
//
// Created by janejntang on 2024/5/7.
//
import Foundation
class RaiseHandApplicationCell: UITableViewCell {
let attendeeModel: RequestEntity
let viewModel: RaiseHandApplicationListViewModel
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.font = UIFont.systemFont(ofSize: 16, weight: .regular)
label.numberOfLines = 1
return label
}()
let applyLabel: UILabel = {
let label = UILabel()
label.text = .applyText
label.textColor = UIColor(0x8F9AB2)
label.backgroundColor = UIColor.clear
label.textAlignment = isRTL ? .right : .left
label.font = UIFont.systemFont(ofSize: 12)
return label
}()
let disagreeStageButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = UIColor(0x6B758A).withAlphaComponent(0.3)
button.setTitle(.disagreeSeatText, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
button.layer.cornerRadius = 6
button.clipsToBounds = true
return button
}()
let agreeStageButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = UIColor(0x1C66E5)
button.setTitle(.agreeSeatText, for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.layer.cornerRadius = 6
button.clipsToBounds = true
return button
}()
let downLineView : UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x4F586B).withAlphaComponent(0.3)
return view
}()
init(attendeeModel: RequestEntity ,viewModel: RaiseHandApplicationListViewModel) {
self.attendeeModel = attendeeModel
self.viewModel = viewModel
super.init(style: .default, reuseIdentifier: "RaiseHandCell")
}
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func constructViewHierarchy() {
contentView.addSubview(avatarImageView)
contentView.addSubview(userLabel)
contentView.addSubview(applyLabel)
contentView.addSubview(agreeStageButton)
contentView.addSubview(disagreeStageButton)
contentView.addSubview(downLineView)
}
func activateConstraints() {
avatarImageView.snp.makeConstraints { make in
make.width.height.equalTo(40.scale375())
make.leading.equalToSuperview()
make.top.equalToSuperview().offset(10.scale375Height())
}
agreeStageButton.snp.makeConstraints { make in
make.width.equalTo(48.scale375())
make.height.equalTo(28.scale375Height())
make.trailing.equalToSuperview()
make.centerY.equalToSuperview()
}
disagreeStageButton.snp.makeConstraints { make in
make.trailing.equalTo(agreeStageButton.snp.leading).offset(-10)
make.centerY.equalTo(agreeStageButton)
make.width.height.equalTo(agreeStageButton)
}
userLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(14.scale375Height())
make.leading.equalTo(avatarImageView.snp.trailing).offset(12.scale375())
make.width.equalTo(150.scale375())
make.height.equalTo(22.scale375())
}
applyLabel.snp.makeConstraints { make in
make.top.equalTo(userLabel.snp.bottom).offset(2.scale375Height())
make.leading.equalTo(userLabel)
}
downLineView.snp.makeConstraints { make in
make.leading.equalTo(userLabel)
make.trailing.equalToSuperview()
make.bottom.equalToSuperview()
make.height.equalTo(1.scale375())
}
}
func bindInteraction() {
setupViewState(item: attendeeModel)
agreeStageButton.addTarget(self, action: #selector(agreeStageAction(sender:)), for: .touchUpInside)
disagreeStageButton.addTarget(self, action: #selector(disagreeStageAction(sender:)), for: .touchUpInside)
}
func setupViewState(item: RequestEntity) {
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
}
userLabel.text = item.userName
backgroundColor = .clear
}
@objc func agreeStageAction(sender: UIButton) {
viewModel.respondRequest(isAgree: true, request: attendeeModel)
}
@objc func disagreeStageAction(sender: UIButton) {
viewModel.respondRequest(isAgree: false, request: attendeeModel)
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var applyText: String {
localized("Apply to be on stage")
}
static var disagreeSeatText: String {
localized("Reject")
}
static var agreeSeatText: String {
localized("Agree")
}
}

View File

@@ -0,0 +1,219 @@
//
// RaiseHandApplicationListView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/13.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
class RaiseHandApplicationListView: UIView {
let viewModel: RaiseHandApplicationListViewModel
let titleLabel: UILabel = {
let label = UILabel()
label.text = .takeSeatApplyTitle
label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(0xD5E0F2)
label.backgroundColor = .clear
return label
}()
let allAgreeButton : UIButton = {
let button = UIButton(type: .custom)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
button.setTitle(.agreeAllText, for: .normal)
button.setTitleColor(UIColor(0xFFFFFF), for: .normal)
button.setBackgroundImage(UIColor(0x1C66E5).withAlphaComponent(0.5).trans2Image(), for: .disabled)
button.setBackgroundImage(UIColor(0x1C66E5).trans2Image(), for: .normal)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.adjustsImageWhenHighlighted = false
return button
}()
let allRejectButton: UIButton = {
let button = UIButton(type: .custom)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .regular)
button.setTitle(.rejectAllText, for: .normal)
button.setTitleColor(UIColor(0xB2BBD1), for: .normal)
button.setBackgroundImage(UIColor(0x4F586B).withAlphaComponent(0.5).trans2Image(), for: .disabled)
button.setBackgroundImage(UIColor(0x4F586B).trans2Image(), for: .normal)
button.layer.cornerRadius = 6
button.clipsToBounds = true
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.adjustsImageWhenHighlighted = false
return button
}()
let placeholderUIImageView: UIImageView = {
let image = UIImage(named: "room_apply_placeholder", in: tuiRoomKitBundle(), compatibleWith: nil)
let imageView = UIImageView(image: image)
return imageView
}()
let placeholderLabel: UILabel = {
let label = UILabel()
label.text = .noMemberApplicationText
label.textColor = UIColor(0xB2BBD1)
label.font = UIFont.systemFont(ofSize: 16, weight: .regular)
return label
}()
lazy var applyTableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = .clear
tableView.register(RaiseHandApplicationCell.self, forCellReuseIdentifier: "RaiseHandCell")
return tableView
}()
init(viewModel: RaiseHandApplicationListViewModel) {
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()
backgroundColor = UIColor(0x22262E)
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(titleLabel)
addSubview(placeholderUIImageView)
addSubview(placeholderLabel)
addSubview(applyTableView)
addSubview(allRejectButton)
addSubview(allAgreeButton)
}
func activateConstraints() {
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(10.scale375Height())
make.leading.equalToSuperview().offset(16.scale375())
}
placeholderUIImageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.width.height.equalTo(48.scale375())
make.centerY.equalToSuperview().offset(-30.scale375Height())
}
placeholderLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(placeholderUIImageView.snp.bottom).offset(8.scale375Height())
make.height.equalTo(22.scale375Height())
}
applyTableView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.top.equalTo(titleLabel.snp.bottom).offset(26.scale375Height())
make.bottom.equalTo(allAgreeButton.snp.top).offset(-10.scale375Height())
}
allRejectButton.snp.remakeConstraints { make in
make.leading.equalTo(applyTableView)
make.bottom.equalToSuperview().offset(-34.scale375Height())
make.height.equalTo(40.scale375Height())
make.width.equalTo(167.scale375())
}
allAgreeButton.snp.makeConstraints { make in
make.trailing.equalTo(applyTableView)
make.bottom.height.width.equalTo(allRejectButton)
}
}
func bindInteraction() {
viewModel.viewResponder = self
allAgreeButton.addTarget(self, action: #selector(allAgreeStageAction(sender:)), for: .touchUpInside)
allRejectButton.addTarget(self, action: #selector(allRejectAction(sender:)), for: .touchUpInside)
setupPlaceholderViewState(isShown: viewModel.isPlaceholderViewShown)
setupApplyButtonState(isEnabled: viewModel.isApplyButtonEnabled)
}
@objc func allAgreeStageAction(sender: UIButton) {
viewModel.respondAllRequest(isAgree: true)
}
@objc func allRejectAction(sender: UIButton) {
viewModel.respondAllRequest(isAgree: false)
}
private func setupPlaceholderViewState(isShown: Bool) {
placeholderLabel.isHidden = !isShown
placeholderUIImageView.isHidden = !isShown
applyTableView.isHidden = isShown
}
private func setupApplyButtonState(isEnabled: Bool) {
allAgreeButton.isEnabled = isEnabled
allRejectButton.isEnabled = isEnabled
}
deinit {
debugPrint("deinit \(self)")
}
}
extension RaiseHandApplicationListView: UITableViewDataSource {
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.inviteSeatList.count
}
}
extension RaiseHandApplicationListView: UITableViewDelegate {
internal func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let attendeeModel = viewModel.inviteSeatList[indexPath.row]
let cell = RaiseHandApplicationCell(attendeeModel: attendeeModel, viewModel: viewModel)
cell.selectionStyle = .none
return cell
}
internal func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60.scale375()
}
}
extension RaiseHandApplicationListView: RaiseHandApplicationListViewResponder {
func updateApplyButtonState(isEnabled: Bool) {
setupApplyButtonState(isEnabled: isEnabled)
}
func updatePlaceholderViewState(isShown: Bool) {
setupPlaceholderViewState(isShown: isShown)
}
func reloadApplyListView() {
applyTableView.reloadData()
}
func makeToast(text: String) {
RoomRouter.makeToastInCenter(toast: text, duration: 1)
}
}
private extension String {
static var takeSeatApplyTitle: String {
localized("Participants apply to come on stage")
}
static var rejectAllText: String {
localized("Reject all")
}
static var noMemberApplicationText: String {
localized("No participants's application yet")
}
static var agreeAllText: String {
localized("Agree to all")
}
}

View File

@@ -0,0 +1,148 @@
//
// RaiseHandApplicationNotificationView.swift
// TUIRoomKit
//
// Created by janejntang on 2024/5/8.
//
import Foundation
protocol RaiseHandApplicationNotificationViewListener: AnyObject {
func onHidden()
func onShown()
}
class RaiseHandApplicationNotificationView: UIView {
let viewModel: RaiseHandApplicationNotificationViewModel
weak var delegate: RaiseHandApplicationNotificationViewListener?
private let imageView: UIImageView = {
let image = UIImage(named: "room_raise_hand_notification", in: tuiRoomKitBundle(), compatibleWith: nil)
return UIImageView(image: image)
}()
private let label: UILabel = {
let label = UILabel()
label.textAlignment = isRTL ? .right : .left
label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
label.textColor = UIColor(0x181820)
label.adjustsFontSizeToFitWidth = false
return label
}()
private let checkButton: UIButton = {
let button = UIButton()
button.backgroundColor = .clear
button.setTitle(.checkText, for: .normal)
button.setTitleColor(UIColor(0x1C66E5), for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium)
button.titleLabel?.textAlignment = .center
return button
}()
init(viewModel: RaiseHandApplicationNotificationViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
self.viewModel.responder = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(imageView)
addSubview(label)
addSubview(checkButton)
}
func activateConstraints() {
imageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalToSuperview().offset(8.scale375Height())
make.width.height.equalTo(24.scale375())
}
checkButton.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().offset(-6.scale375())
make.height.equalTo(22.scale375Height())
make.width.equalTo(48.scale375())
}
label.snp.makeConstraints { make in
make.leading.equalTo(imageView.snp.trailing).offset(10.scale375())
make.trailing.equalTo(checkButton.snp.leading).offset(-10.scale375())
make.centerY.equalToSuperview()
make.height.equalTo(22.scale375Height())
}
}
func bindInteraction() {
isHidden = true
backgroundColor = UIColor(0xFFFFFF)
layer.cornerRadius = 6
checkButton.addTarget(self, action: #selector(checkAction(sender:)), for: .touchUpInside)
guard viewModel.isShownRaiseHandApplicationNotificationView else { return }
guard let userId = viewModel.userId, let userName = viewModel.userName, let count = viewModel.applicationCount else { return }
show(userId: userId, userName: userName, count: count)
}
@objc private func checkAction(sender: UIButton) {
hide()
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hide), object: nil)
viewModel.checkRaiseHandApplicationAction()
}
func show(userId: String, userName: String, count: Int) {
isHidden = false
let nameText = userName ?? userId
let title = count > 1 ?
.multiApplyingOnStageText.replacingOccurrences(of: "xx", with: nameText).replacingOccurrences(of: "yy", with: String(count))
: localizedReplace(.singleApplyingOnStageText, replace: nameText)
label.text = title
delegate?.onShown()
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hide), object: nil)
guard viewModel.delayDisappearanceTime > 0 else { return }
perform(#selector(hide), with: nil, afterDelay: viewModel.delayDisappearanceTime)
}
@objc func hide() {
isHidden = true
delegate?.onHidden()
}
deinit {
NSObject.cancelPreviousPerformRequests(withTarget: self)
}
}
extension RaiseHandApplicationNotificationView: RaiseHandApplicationNotificationViewModelResponder {
func showRaiseHandApplicationNotificationView(userId: String, userName: String, count: Int) {
show(userId: userId, userName: userName, count: count)
}
func hideRaiseHandApplicationNotificationView() {
hide()
}
}
private extension String {
static var checkText: String {
localized("Check")
}
static var singleApplyingOnStageText: String {
localized("xx is applying to be on stage.")
}
static var multiApplyingOnStageText: String {
localized("Including xx, yy people are applying to be on stage.")
}
}

View File

@@ -0,0 +1,81 @@
//
// TopItemView.swift
// TUIRoomKit
//
// Created by janejntang on 2022/12/30.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
class TopItemView: UIView {
let itemData: ButtonItemData
let button: UIButton = {
let button = UIButton(type: .custom)
return button
}()
// MARK: - initialized function
init(itemData: ButtonItemData) {
self.itemData = itemData
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
self.layer.cornerRadius = 10
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(button)
}
func activateConstraints() {
button.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview()
make.width.height.equalTo(24)
}
}
func bindInteraction() {
setupViewState(item: itemData)
button.addTarget(self, action: #selector(clickMenuButton(sender:)), for: .touchUpInside)
}
func setupViewState(item: ButtonItemData) {
button.isSelected = item.isSelect
button.isEnabled = item.isEnabled
button.isHidden = item.isHidden
if let normalImage = item.normalImage {
button.setImage(normalImage, for: .normal)
}
if let selectedImage = item.selectedImage {
button.setImage(selectedImage, for: .selected)
}
if let disabledImage = item.disabledImage {
button.setImage(disabledImage, for: .disabled)
}
}
@objc
func clickMenuButton(sender: UIButton) {
itemData.action?(sender)
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,297 @@
//
// TopView.swift
// TUIRoomKit
//
// Created by janejntang on 2022/12/30.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
class TopView: UIView {
// MARK: - store property
let viewModel: TopViewModel
private var viewArray: [TopItemView] = []
let backgroundImageView: UIImageView = {
let image = UIImage(named: "room_top_background",in: tuiRoomKitBundle(), compatibleWith: nil)
let imageView = UIImageView(image: image)
return imageView
}()
let contentView: UIView = {
let view = UIView()
return view
}()
let stackView: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.alignment = .center
view.spacing = 2.scale375()
return view
}()
let meetingTitleView: UIView = {
let view = UIView()
return view
}()
let meetingNameLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xD5E0F2)
label.font = UIFont(name: "PingFangSC-Medium", size: 16)
label.textAlignment = isRTL ? .right : .left
label.lineBreakMode = .byTruncatingTail
return label
}()
let dropDownButton: UIButton = {
let button = UIButton(type: .custom)
let normalIcon = UIImage(named: "room_drop_down", in: tuiRoomKitBundle(), compatibleWith: nil)
button.setImage(normalIcon, for: .normal)
button.isEnabled = true
return button
}()
let timeLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xD1D9EC)
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
label.font = UIFont(name: "PingFangSC-Medium", size: 12)
return label
}()
let exitView: UIView = {
let view = UIView()
return view
}()
let exitImage: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "room_exit", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn()
return imageView
}()
let exitLabel: UILabel = {
let label = UILabel()
label.text = .exitText
label.textColor = UIColor(0xED414D)
label.textAlignment = .right
label.adjustsFontSizeToFitWidth = true
label.font = UIFont(name: "PingFangSC-Medium", size: 14)
return label
}()
var menuButtons: [UIView] = []
// MARK: - initialized function
init(viewModel: TopViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
backgroundColor = UIColor(0x0F1014)
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(backgroundImageView)
addSubview(contentView)
contentView.addSubview(meetingTitleView)
contentView.addSubview(stackView)
contentView.addSubview(exitView)
meetingTitleView.addSubview(meetingNameLabel)
meetingTitleView.addSubview(dropDownButton)
meetingTitleView.addSubview(timeLabel)
exitView.addSubview(exitImage)
exitView.addSubview(exitLabel)
for item in viewModel.viewItems {
let view = TopItemView(itemData: item)
menuButtons.append(view)
stackView.addArrangedSubview(view)
viewArray.append(view)
let size = item.size ?? CGSize(width: 35.scale375(), height: 40.scale375Height())
view.snp.makeConstraints { make in
make.height.equalTo(size.height)
make.width.equalTo(size.width)
}
}
}
func activateConstraints() {
updateRootViewOrientation(isLandscape: isLandscape)
backgroundImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
meetingTitleView.snp.makeConstraints { make in
make.width.lessThanOrEqualTo(180.scale375())
make.height.equalTo(44.scale375Height())
make.center.equalToSuperview()
}
stackView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16.scale375())
make.top.bottom.equalToSuperview()
}
meetingNameLabel.snp.makeConstraints { make in
make.top.equalToSuperview()
make.height.equalTo(24.scale375())
make.width.lessThanOrEqualTo(128.scale375())
make.leading.equalToSuperview()
}
timeLabel.snp.makeConstraints { make in
make.top.equalTo(meetingNameLabel.snp.bottom).offset(5)
make.centerX.equalToSuperview()
make.height.equalTo(20)
}
exitView.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.trailing.equalToSuperview().offset(-16.scale375())
make.width.equalTo(51.scale375())
}
exitImage.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview()
make.width.equalTo(20.scale375())
make.height.equalTo(20.scale375())
}
exitLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview()
make.width.equalTo(28.scale375())
make.height.equalTo(20.scale375())
}
}
func bindInteraction() {
let dropTap = UITapGestureRecognizer(target: self, action: #selector(dropDownAction(sender:)))
let exitTap = UITapGestureRecognizer(target: self, action: #selector(exitAction(sender:)))
meetingNameLabel.text = viewModel.engineManager.store.roomInfo.name
meetingTitleView.addGestureRecognizer(dropTap)
exitView.addGestureRecognizer(exitTap)
viewModel.viewResponder = self
}
func updateRootViewOrientation(isLandscape: Bool) {
contentView.snp.remakeConstraints { make in
if isLandscape {
make.top.equalToSuperview()
} else {
make.top.equalToSuperview().offset(44.scale375Height())
}
make.leading.trailing.bottom.equalToSuperview()
}
meetingTitleView.snp.remakeConstraints { make in
if isLandscape {
make.width.lessThanOrEqualTo(300.scale375())
make.height.equalTo(24.scale375Height())
} else {
make.width.lessThanOrEqualTo(180.scale375())
make.height.equalTo(44.scale375Height())
}
make.center.equalToSuperview()
}
dropDownButton.snp.remakeConstraints { make in
make.leading.equalTo(meetingNameLabel.snp.trailing).offset(2.scale375())
make.centerY.equalTo(meetingNameLabel)
make.width.height.equalTo(16.scale375())
if !isLandscape {
make.trailing.equalToSuperview()
}
}
timeLabel.snp.remakeConstraints { make in
if isLandscape {
make.centerY.equalTo(dropDownButton)
make.leading.equalTo(dropDownButton.snp.trailing).offset(15)
make.trailing.equalToSuperview()
} else {
make.top.equalTo(meetingNameLabel.snp.bottom).offset(5)
make.centerX.equalToSuperview()
}
}
}
@objc func dropDownAction(sender: UIView) {
viewModel.dropDownAction(sender: sender)
}
@objc func exitAction(sender: UIView) {
viewModel.exitAction(sender: sender)
}
deinit {
debugPrint("deinit \(self)")
}
}
enum AlertAction {
case dismissRoomAction
case transferMasterAction
case leaveRoomAction
case cancelAction
}
extension TopView: TopViewModelResponder {
func updateMeetingNameLabel(_ text: String) {
meetingNameLabel.text = text
}
func updateStackView(item: ButtonItemData) {
guard let view = viewArray.first(where: { $0.itemData.buttonType == item.buttonType }) else { return }
view.setupViewState(item: item)
}
func updateTimerLabel(text: String) {
self.timeLabel.text = text
}
#if RTCube_APPSTORE
func showReportView() {
let selector = NSSelectorFromString("showReportAlertWithRoomId:ownerId:")
if responds(to: selector) {
let roomInfo = viewModel.store.roomInfo
perform(selector, with: roomInfo.roomId, with: roomInfo.ownerId)
}
}
#endif
}
private extension String {
static var leaveRoomTitle: String {
localized("Are you sure you want to leave the conference?")
}
static var destroyRoomTitle: String {
localized("Are you sure you want to end the conference?")
}
static var dismissMeetingTitle: String {
localized("If you don't want to end the conference")
}
static var appointNewHostText: String {
localized("Please appoint a new host before leaving the conference")
}
static var leaveMeetingText: String {
localized("Leave conference")
}
static var dismissMeetingText: String {
localized("End conference")
}
static var cancelText: String {
localized("Cancel")
}
static var exitText: String {
localized("Exit")
}
}

View File

@@ -0,0 +1,356 @@
//
// TransferMasterView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/2/20.
//
import Foundation
class TransferMasterView: UIView {
let viewModel: TransferMasterViewModel
var attendeeList: [UserEntity]
var searchArray: [UserEntity] = []
private var isSearching: Bool = false
let topLabel: UILabel = {
let label = UILabel()
label.textColor = UIColor(0xD5E0F2)
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
label.text = .transferMasterText
return label
}()
let searchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.placeholder = .searchMemberText
searchBar.setBackgroundImage(UIColor(0x1B1E26).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 appointMasterButton: UIButton = {
let button = UIButton(type: .custom)
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
button.setTitle(.appointAndLeaveRoomText, for: .normal)
button.setTitleColor(UIColor(0xFFFFFF), for: .normal)
button.setBackgroundImage(UIColor(0x006EFF).withAlphaComponent(0.2).trans2Image(), for: .disabled)
button.setBackgroundImage(UIColor(0x006EFF).trans2Image(), for: .normal)
button.layer.cornerRadius = 12
button.clipsToBounds = true
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.adjustsImageWhenHighlighted = false
return button
}()
lazy var transferMasterTableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.separatorStyle = .none
tableView.delegate = self
tableView.dataSource = self
tableView.backgroundColor = UIColor(0x1B1E26)
tableView.register(TransferMasterTableCell.self, forCellReuseIdentifier: "RaiseHandCell")
return tableView
}()
init(viewModel: TransferMasterViewModel) {
self.viewModel = viewModel
self.attendeeList = viewModel.attendeeList
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()
backgroundColor = UIColor(0x1B1E26)
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
func constructViewHierarchy() {
addSubview(topLabel)
addSubview(searchBar)
addSubview(transferMasterTableView)
addSubview(appointMasterButton)
addSubview(searchControl)
}
func activateConstraints() {
topLabel.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.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.height.equalTo(34.scale375Height())
make.top.equalTo(topLabel.snp.bottom).offset(23.scale375Height())
}
transferMasterTableView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16.scale375())
make.trailing.equalToSuperview().offset(-16.scale375())
make.top.equalTo(searchBar.snp.bottom).offset(10.scale375Height())
make.bottom.equalToSuperview()
}
appointMasterButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-20)
make.bottom.equalToSuperview().offset(-40 - kDeviceSafeBottomHeight)
make.height.equalTo(50)
make.leading.equalToSuperview().offset(20)
}
searchControl.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
func bindInteraction() {
searchBar.delegate = self
viewModel.viewResponder = self
appointMasterButton.addTarget(self, action: #selector(appointMasterAction(sender:)), for: .touchUpInside)
let tap = UITapGestureRecognizer(target: self, action: #selector(hideSearchControl(sender:)))
searchControl.addGestureRecognizer(tap)
appointMasterButton.isEnabled = viewModel.userId.count > 0
}
@objc func appointMasterAction(sender: UIButton) {
viewModel.appointMasterAction(sender: sender)
}
@objc func hideSearchControl(sender: UIView) {
if #available(iOS 13, *) {
searchBar.searchTextField.resignFirstResponder()
} else {
searchBar.resignFirstResponder()
}
searchControl.isHidden = true
}
deinit {
debugPrint("deinit \(self)")
}
}
extension TransferMasterView: UISearchBarDelegate {
func searchBar(_ searchBar:UISearchBar,textDidChange searchText:String){
let searchContentText = searchText.trimmingCharacters(in: .whitespaces)
if searchContentText.count == 0 {
attendeeList = viewModel.attendeeList
transferMasterTableView.reloadData()
isSearching = false
} else {
searchArray = viewModel.attendeeList.filter({ model -> Bool in
return model.userName.contains(searchContentText)
})
attendeeList = searchArray
transferMasterTableView.reloadData()
isSearching = true
}
}
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
searchControl.isHidden = false
return true
}
}
extension TransferMasterView: UITableViewDataSource {
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return attendeeList.count
}
}
extension TransferMasterView: UITableViewDelegate {
internal func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let attendeeModel = attendeeList[indexPath.row]
let cell = TransferMasterTableCell(attendeeModel: attendeeModel, viewModel: viewModel)
cell.selectionStyle = .none
return cell
}
internal func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
searchBar.endEditing(true)
if #available(iOS 13, *) {
searchBar.searchTextField.resignFirstResponder()
} else {
searchBar.resignFirstResponder()
}
viewModel.userId = attendeeList[indexPath.row].userId
appointMasterButton.isEnabled = true
transferMasterTableView.reloadData()
}
internal func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60.scale375Height()
}
}
extension TransferMasterView: TransferMasterViewResponder {
func makeToast(message: String) {
makeToast(message)
}
func reloadTransferMasterTableView() {
guard !isSearching else { return }
attendeeList = viewModel.attendeeList
transferMasterTableView.reloadData()
}
func searchControllerChangeActive(isActive: Bool) {
searchBar.endEditing(!isActive)
if #available(iOS 13, *) {
searchBar.searchTextField.resignFirstResponder()
} else {
searchBar.resignFirstResponder()
}
}
}
class TransferMasterTableCell: UITableViewCell {
let attendeeModel: UserEntity
let viewModel: TransferMasterViewModel
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 checkMarkButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "room_check_mark", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
button.isHidden = true
return button
}()
let downLineView : UIView = {
let view = UIView()
view.backgroundColor = UIColor(0x2A2D38)
return view
}()
init(attendeeModel: UserEntity ,viewModel: TransferMasterViewModel) {
self.attendeeModel = attendeeModel
self.viewModel = viewModel
super.init(style: .default, reuseIdentifier: "TransferMasterTableCell")
}
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func constructViewHierarchy() {
contentView.addSubview(avatarImageView)
contentView.addSubview(userLabel)
contentView.addSubview(checkMarkButton)
contentView.addSubview(downLineView)
}
func activateConstraints() {
avatarImageView.snp.makeConstraints { make in
make.width.height.equalTo(40.scale375Height())
make.leading.equalToSuperview()
make.centerY.equalToSuperview()
}
checkMarkButton.snp.makeConstraints { make in
make.width.height.equalTo(22.scale375())
make.trailing.equalToSuperview()
make.centerY.equalTo(self.avatarImageView)
}
userLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.leading.equalTo(avatarImageView.snp.trailing).offset(10.scale375())
make.width.equalTo(150.scale375())
make.height.equalTo(22.scale375Height())
}
downLineView.snp.makeConstraints { make in
make.leading.equalTo(userLabel)
make.trailing.equalToSuperview()
make.bottom.equalToSuperview()
make.height.equalTo(1.scale375Height())
}
}
func bindInteraction() {
backgroundColor = UIColor(0x1B1E26)
setupViewState(item: attendeeModel)
}
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
}
userLabel.text = item.userName
if viewModel.userId == attendeeModel.userId {
checkMarkButton.isHidden = false
}
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var transferMasterText: String {
localized("Appoint a new master")
}
static var searchMemberText: String {
localized("Search for participants")
}
static var appointAndLeaveRoomText: String {
localized("Appoint and leave")
}
}

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")
}
}

View File

@@ -0,0 +1,175 @@
//
// ScreenCaptureMaskView.swift
// TUIRoomKit
//
// Created by janejntang on 2023/7/17.
// Turn on masked View for screen sharing
//
import Foundation
enum ScreenCaptureMaskViewFrameType {
case fullScreen
case small
}
class ScreenCaptureMaskView: UIView {
private var dotsTimer: Timer = Timer()
weak var responder: TUIVideoSeatViewResponder?
let frameType: ScreenCaptureMaskViewFrameType
let contentView: UIView = {
let view = UIView()
return view
}()
let sharingScreenView: UIView = {
let view = UIView()
return view
}()
let sharingScreenImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "room_sharingScreen", in: tuiRoomKitBundle(), compatibleWith: nil)
return imageView
}()
let sharingScreenLabel: UILabel = {
let label = UILabel()
label.text = .sharingScreenText
label.textColor = UIColor(0xB2BBD1)
label.font = UIFont(name: "PingFangSC-Regular", size: 16)
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
return label
}()
let stopScreenButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle(.shareOffText, for: .normal)
button.backgroundColor = UIColor(0xCC3D47)
button.layer.cornerRadius = 6.scale375()
button.titleLabel?.font = UIFont(name: "PingFangSC-Regular", size: 12)
return button
}()
init(frameType: ScreenCaptureMaskViewFrameType) {
self.frameType = frameType
super.init(frame: .zero)
updateLabelText()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - view layout
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
backgroundColor = UIColor(0x1B1E26)
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
private func constructViewHierarchy() {
addSubview(contentView)
contentView.addSubview(sharingScreenView)
contentView.addSubview(stopScreenButton)
sharingScreenView.addSubview(sharingScreenImageView)
sharingScreenView.addSubview(sharingScreenLabel)
}
private func activateConstraints() {
contentView.snp.makeConstraints{ make in
make.centerX.centerY.equalToSuperview()
make.width.equalTo(128.scale375())
make.height.equalTo(132.scale375())
}
sharingScreenView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview()
make.height.equalTo(74.scale375())
}
stopScreenButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview()
make.width.equalTo(102.scale375())
make.height.equalTo(34.scale375())
}
sharingScreenImageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalToSuperview()
make.height.equalTo(48.scale375())
make.width.equalTo(48.scale375())
}
sharingScreenLabel.snp.makeConstraints { make in
make.bottom.left.right.equalToSuperview()
make.height.equalTo(22.scale375())
}
}
private func bindInteraction() {
stopScreenButton.addTarget(self, action: #selector(stopScreenCaptureAction(sender:)), for: .touchUpInside)
addGesture()
}
private func addGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(clickMask))
addGestureRecognizer(tap)
}
@objc func stopScreenCaptureAction(sender: UIButton) {
RoomRouter.presentAlert(title: .toastTitleText, message: .toastMessageText, sureTitle: .toastStopText, declineTitle: .toastCancelText, sureBlock: { [weak self] in
guard let self = self else { return }
self.responder?.stopScreenCapture()
}, declineBlock: nil)
}
@objc func clickMask() {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, param: [:])
guard RoomRouter.shared.hasChatWindow() else { return }
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_HiddenChatWindow, param: [:])
}
func updateLabelText() {
var dots = ""
dotsTimer = Timer(timeInterval: 1.0, repeats: true) { [weak self] timer in
guard let self = self else { return }
if dots.count == 3 {
dots.removeAll()
}
dots.append(".")
self.sharingScreenLabel.text? = .sharingScreenText + dots
}
RunLoop.current.add(dotsTimer, forMode: .default)
}
deinit {
dotsTimer.invalidate()
debugPrint("deinit:\(self)")
}
}
private extension String {
static var sharingScreenText: String {
localized("You are sharing your screen")
}
static var shareOffText: String {
localized("Stop")
}
static var toastTitleText: String {
localized("Share Screen")
}
static var toastMessageText: String {
localized("Stop TUIRoom screen sharing screen live?")
}
static var toastCancelText: String {
localized("Cancel")
}
static var toastStopText: String {
localized("Stop")
}
}

View File

@@ -0,0 +1,242 @@
//
// VideoSeatCell.swift
// TUIVideoSeat
//
// Created by WesleyLei on 2021/12/16.
// Copyright © 2021 Tencent. All rights reserved.
//
import SnapKit
import UIKit
class VideoSeatCell: UICollectionViewCell {
var seatItem: VideoSeatItem?
var isSupportedAmplification: Bool {
return seatItem?.videoStreamType == .screenStream
}
private lazy var scrollRenderView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = UIColor(0x17181F)
scrollView.layer.cornerRadius = 16
scrollView.layer.masksToBounds = true
scrollView.layer.borderWidth = 2
scrollView.layer.borderColor = UIColor.clear.cgColor
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.maximumZoomScale = 5
scrollView.minimumZoomScale = 1
scrollView.isScrollEnabled = false
scrollView.delegate = self
return scrollView
}()
let renderView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
return view
}()
let backgroundMaskView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = UIColor(0x17181F)
view.layer.cornerRadius = 16
view.layer.masksToBounds = true
return view
}()
let userInfoView: VideoSeatUserStatusView = {
let view = VideoSeatUserStatusView()
return view
}()
let avatarImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.layer.masksToBounds = true
return imageView
}()
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
contentView.backgroundColor = .clear
}
private func constructViewHierarchy() {
scrollRenderView.addSubview(renderView)
scrollRenderView.addSubview(backgroundMaskView)
contentView.addSubview(scrollRenderView)
contentView.addSubview(avatarImageView)
contentView.addSubview(userInfoView)
}
private func activateConstraints() {
scrollRenderView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(2)
}
renderView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalToSuperview()
make.height.equalToSuperview()
}
backgroundMaskView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
userInfoView.snp.makeConstraints { make in
make.height.equalTo(24)
make.bottom.equalToSuperview().offset(-5)
make.leading.equalToSuperview().offset(5)
make.width.lessThanOrEqualTo(self).multipliedBy(0.9)
}
}
@objc private func resetVolumeView() {
guard let seatItem = seatItem else { return }
userInfoView.updateUserVolume(hasAudio: seatItem.hasAudioStream, volume: 0)
scrollRenderView.layer.borderColor = UIColor.clear.cgColor
}
override func prepareForReuse() {
scrollRenderView.zoomScale = 1.0
}
deinit {
NSObject.cancelPreviousPerformRequests(withTarget: self)
debugPrint("deinit \(self)")
}
}
extension VideoSeatCell: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return isSupportedAmplification ? renderView : nil
}
}
// MARK: - Public
extension VideoSeatCell {
func updateUI(item: VideoSeatItem) {
seatItem = item
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
avatarImageView.sd_setImage(with: URL(string: item.avatarUrl), placeholderImage: placeholder)
avatarImageView.isHidden = item.videoStreamType == .screenStream ? true : item.hasVideoStream
backgroundMaskView.isHidden = item.videoStreamType == .screenStream ? true : item.hasVideoStream
userInfoView.updateUserStatus(item)
resetVolumeView()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
let width = min(self.mm_w / 2, 72)
self.avatarImageView.layer.cornerRadius = width * 0.5
guard let _ = self.avatarImageView.superview else { return }
self.avatarImageView.snp.remakeConstraints { make in
make.height.width.equalTo(width)
make.center.equalToSuperview()
}
}
}
func updateUIVolume(item: VideoSeatItem) {
userInfoView.updateUserVolume(hasAudio: item.hasAudioStream, volume: item.userVoiceVolume)
if item.userVoiceVolume > 0 && item.hasAudioStream {
if item.videoStreamType != .screenStream {
scrollRenderView.layer.borderColor = UIColor(0xA5FE33).cgColor
}
} else {
scrollRenderView.layer.borderColor = UIColor.clear.cgColor
}
resetVolume()
}
func resetVolume() {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(resetVolumeView), object: nil)
perform(#selector(resetVolumeView), with: nil, afterDelay: 1)
}
}
class TUIVideoSeatDragCell: VideoSeatCell {
typealias DragCellClickBlock = () -> Void
var clickBlock: DragCellClickBlock?
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
bindInteraction()
}
private func bindInteraction() {
addGesture()
}
func updateSize(size: CGSize) {
var frame = self.frame
frame.size = size
self.frame = frame
center = adsorption(centerPoint: center)
}
}
// MARK: - gesture
extension TUIVideoSeatDragCell {
private func addGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(click))
addGestureRecognizer(tap)
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(dragViewDidDrag(gesture:)))
addGestureRecognizer(dragGesture)
}
@objc private func click() {
clickBlock?()
}
@objc private func dragViewDidDrag(gesture: UIPanGestureRecognizer) {
guard let viewSuperview = superview else { return }
let moveState = gesture.state
let viewCenter = center
switch moveState {
case .changed:
let point = gesture.translation(in: viewSuperview)
center = CGPoint(x: viewCenter.x + point.x, y: viewCenter.y + point.y)
break
case .ended:
let point = gesture.translation(in: viewSuperview)
let newPoint = CGPoint(x: viewCenter.x + point.x, y: viewCenter.y + point.y)
UIView.animate(withDuration: 0.2) {
self.center = self.adsorption(centerPoint: newPoint)
}
break
default: break
}
gesture.setTranslation(.zero, in: viewSuperview)
}
private func adsorption(centerPoint: CGPoint) -> CGPoint {
guard let viewSuperview = superview else { return centerPoint }
let limitMargin = 5.0
let frame = self.frame
let point = CGPoint(x: centerPoint.x - frame.width / 2, y: centerPoint.y - frame.height / 2)
var newPoint = point
if centerPoint.x < (viewSuperview.frame.width / 2) {
newPoint.x = limitMargin
} else {
newPoint.x = viewSuperview.frame.width - frame.width - limitMargin
}
if point.y <= limitMargin {
newPoint.y = limitMargin
} else if (point.y + frame.height) > (viewSuperview.frame.height - limitMargin) {
newPoint.y = viewSuperview.frame.height - frame.height - limitMargin
}
return CGPoint(x: newPoint.x + frame.width / 2, y: newPoint.y + frame.height / 2)
}
}

View File

@@ -0,0 +1,213 @@
//
// VideoSeatLayout.swift
// TUIVideoSeat
//
// Created by janejntang on 2023/3/16.
//
import Foundation
protocol VideoSeatLayoutDelegate: AnyObject {
func updateNumberOfPages(numberOfPages: NSInteger)
}
class VideoSeatLayout: UICollectionViewFlowLayout {
private var prePageCount: NSInteger = 1
private var collectionViewHeight: CGFloat {
return collectionView?.bounds.height ?? UIScreen.main.bounds.height
}
private var collectionViewWidth: CGFloat {
return collectionView?.bounds.width ?? kScreenWidth
}
private var isPortrait: Bool {
return collectionViewHeight > collectionViewWidth
}
private var kVideoSeatCellNumberOfOneRow: CGFloat {
return isPortrait ? 2 : 3
}
private var kMaxShowCellCount: Int {
return 6
}
private let itemDiffSpace: CGFloat = 5.0
private var itemWidthHeight: CGFloat {
let minimumDistance = min(collectionViewHeight, collectionViewWidth)
let availableSpace = minimumDistance - (kVideoSeatCellNumberOfOneRow + 1) * itemDiffSpace
if isPortrait {
return availableSpace / kVideoSeatCellNumberOfOneRow
} else {
return availableSpace / (CGFloat(kMaxShowCellCount) / kVideoSeatCellNumberOfOneRow)
}
}
private let viewModel: TUIVideoSeatViewModel
fileprivate var layoutAttributeArray: [UICollectionViewLayoutAttributes] = []
init(viewModel: TUIVideoSeatViewModel) {
self.viewModel = viewModel
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
super.prepare()
calculateEachCellFrame()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttributeArray
}
override var collectionViewContentSize: CGSize {
return CGSize(width: CGFloat(prePageCount) * collectionViewWidth, height: collectionViewHeight)
}
weak var delegate: VideoSeatLayoutDelegate?
func getMiniscreenFrame(item: VideoSeatItem?) -> CGRect {
var height = isPortrait ? 180.0 : 100.0
var width = isPortrait ? 100.0 : 180.0
if let item = item, !item.hasVideoStream {
height = 100.0
width = 100.0
}
return CGRect(x: collectionViewWidth - width - itemDiffSpace, y: itemDiffSpace, width: width, height: height)
}
}
// MARK: - layout
extension VideoSeatLayout {
private func calculateEachCellFrame() {
guard let collectionViewWidth: CGFloat = collectionView?.bounds.width else { return }
guard viewModel.listSeatItem.count > 0 else { return }
layoutAttributeArray = []
let section: Int = 0
prePageCount = 1
if viewModel.videoSeatViewType == .singleType {
let indexPath = IndexPath(item: 0, section: 0)
let cell = getFullScreenAttributes(indexPath: indexPath)
layoutAttributeArray.append(cell)
} else if viewModel.videoSeatViewType == .largeSmallWindowType {
let largeIndexPath = IndexPath(item: 0, section: section)
let largeCell = getFullScreenAttributes(indexPath: largeIndexPath)
layoutAttributeArray.append(largeCell)
let smallIndexPath = IndexPath(item: 1, section: section)
let smallCell = getSmallAttributes(indexPath: smallIndexPath)
layoutAttributeArray.append(smallCell)
} else if viewModel.videoSeatViewType == .pureAudioType || viewModel.videoSeatViewType == .equallyDividedType {
guard let itemCount = collectionView?.numberOfItems(inSection: section) else { return }
let isMultipage = itemCount >= kMaxShowCellCount
for i in 0 ... itemCount - 1 {
let indexPath = IndexPath(item: i, section: section)
var cell: UICollectionViewLayoutAttributes
if isMultipage {
cell = getMultipageEquallyDividedAttributes(indexPath: indexPath, item: i, itemCount: itemCount, leftDiff: 0.0)
} else {
cell = getEquallyDividedAttributes(indexPath: indexPath, item: i, itemCount: itemCount, leftDiff: 0.0)
}
layoutAttributeArray.append(cell)
}
prePageCount = Int(ceil(CGFloat(itemCount) / CGFloat(kMaxShowCellCount)))
} else if viewModel.videoSeatViewType == .speechType {
guard let itemCount = collectionView?.numberOfItems(inSection: section) else { return }
let isMultipage = (itemCount - 1) >= kMaxShowCellCount
for i in 0 ... itemCount {
let indexPath = IndexPath(item: i, section: section)
var cell: UICollectionViewLayoutAttributes
if i == 0 {
cell = getFullScreenAttributes(indexPath: indexPath)
} else if isMultipage {
cell = getMultipageEquallyDividedAttributes(indexPath: indexPath, item: i - 1,
itemCount: itemCount - 1,
leftDiff: collectionViewWidth)
} else {
cell = getEquallyDividedAttributes(indexPath: indexPath, item: i - 1,
itemCount: itemCount - 1,
leftDiff: collectionViewWidth)
}
layoutAttributeArray.append(cell)
}
prePageCount = Int(ceil(CGFloat(itemCount - 1) / CGFloat(kMaxShowCellCount))) + 1
}
delegate?.updateNumberOfPages(numberOfPages: prePageCount)
}
// Full screen cell layout information
private func getFullScreenAttributes(indexPath: IndexPath) ->
UICollectionViewLayoutAttributes {
let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
cell.frame = CGRect(x: 0, y: 0, width: collectionViewWidth, height: collectionViewHeight)
return cell
}
private func getSmallAttributes(indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
cell.frame = getMiniscreenFrame(item: nil)
return cell
}
private func getEquallyDividedAttributes(indexPath: IndexPath, item: Int, itemCount: Int, leftDiff: CGFloat) ->
UICollectionViewLayoutAttributes {
/*-----------------item&page&currentPageItemCount&cell-----------------**/
let item = item + 1
let page = Int(ceil(CGFloat(item) / CGFloat(kMaxShowCellCount)))
let currentPageItemCount = min(itemCount, page * kMaxShowCellCount) - (page - 1) * kMaxShowCellCount // Number of items on the current page
let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
/*-----------------currentPageAllRow&beginCellY&beginCellLeft-----------------**/
let currentPageAllRow = Int(ceil(CGFloat(currentPageItemCount) / CGFloat(kVideoSeatCellNumberOfOneRow))) // Calculate the total number of rows on this page
let itemAllHeight = (itemWidthHeight + itemDiffSpace) * CGFloat(currentPageAllRow) - itemDiffSpace
let itemAllWidth = (itemWidthHeight + itemDiffSpace) * kVideoSeatCellNumberOfOneRow - itemDiffSpace
let beginCellY = (collectionViewHeight - itemAllHeight) * 0.5 // Calculate beginCellTop
let beginCellX = (collectionViewWidth - itemAllWidth) * 0.5 // Calculate beginCellTop
let beginCellLeft = CGFloat(page - 1) * collectionViewWidth // Calculate beginCellLeft
/*-----------------itemIndex&column&row-----------------**/
let itemIndex = item - (page - 1) * kMaxShowCellCount // What is the number on this page?
let column = (itemIndex - 1) % Int(kVideoSeatCellNumberOfOneRow) // Which column of cell is on the current page starting from 0?
let row = Int(ceil(CGFloat(itemIndex) / CGFloat(kVideoSeatCellNumberOfOneRow))) // What is the row of cell on the current page?
let itemY = beginCellY + (itemWidthHeight + itemDiffSpace) * CGFloat(row - 1)
var itemX = 0.0
if currentPageAllRow == row {
// Adjust the center of the last row
let lastRowItemCount = currentPageItemCount - (row - 1) * Int(kVideoSeatCellNumberOfOneRow)
let lastRowBeginCellLeft = (collectionViewWidth - (itemWidthHeight + itemDiffSpace) * CGFloat(lastRowItemCount) - itemDiffSpace) * 0.5
itemX = lastRowBeginCellLeft + beginCellLeft + (itemWidthHeight + itemDiffSpace) * CGFloat(column)
} else {
itemX = beginCellX + beginCellLeft + (itemWidthHeight + itemDiffSpace) * CGFloat(column)
}
cell.frame = CGRect(x: leftDiff + itemX, y: itemY, width: itemWidthHeight, height: itemWidthHeight)
return cell
}
private func getMultipageEquallyDividedAttributes(indexPath: IndexPath, item: Int, itemCount: Int, leftDiff: CGFloat) ->
UICollectionViewLayoutAttributes {
let item = item + 1
let page = Int(ceil(CGFloat(item) / CGFloat(kMaxShowCellCount)))
let cell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let currentPageAllRow = kMaxShowCellCount / Int(kVideoSeatCellNumberOfOneRow)
let itemAllHeight = (itemWidthHeight + itemDiffSpace) * CGFloat(currentPageAllRow) - itemDiffSpace
let itemAllWidth = (itemWidthHeight + itemDiffSpace) * kVideoSeatCellNumberOfOneRow - itemDiffSpace
let beginCellY = (collectionViewHeight - itemAllHeight) * 0.5
let beginCellX = (collectionViewWidth - itemAllWidth) * 0.5
let beginCellLeft = CGFloat(page - 1) * collectionViewWidth
let itemIndex = item - (page - 1) * kMaxShowCellCount
let column = (itemIndex - 1) % Int(kVideoSeatCellNumberOfOneRow)
let row = Int(ceil(CGFloat(itemIndex) / CGFloat(kVideoSeatCellNumberOfOneRow)))
let itemY = beginCellY + (itemWidthHeight + itemDiffSpace) * CGFloat(row - 1)
let itemX = beginCellX + beginCellLeft + (itemWidthHeight + itemDiffSpace) * CGFloat(column)
cell.frame = CGRect(x: leftDiff + itemX, y: itemY, width: itemWidthHeight, height: itemWidthHeight)
return cell
}
}

View File

@@ -0,0 +1,106 @@
//
// VideoSeatUserStatusView.swift
// TUIVideoSeat
//
// Created by jack on 2023/3/6.
// Copyright © 2023 Tencent. All rights reserved.
import Foundation
class VideoSeatUserStatusView: UIView {
private var isShownHomeOwnerImageView: Bool = false
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
backgroundColor = UIColor(0x22262E, alpha: 0.8)
layer.cornerRadius = 12
layer.masksToBounds = true
}
private func constructViewHierarchy() {
addSubview(homeOwnerImageView)
addSubview(voiceVolumeImageView)
addSubview(userNameLabel)
}
private let homeOwnerImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil))
imageView.layer.cornerRadius = 12
imageView.layer.masksToBounds = true
return imageView
}()
private let userNameLabel: UILabel = {
let user = UILabel()
user.textColor = .white
user.backgroundColor = UIColor.clear
user.textAlignment = isRTL ? .right : .left
user.numberOfLines = 1
user.font = UIFont(name: "PingFangSC-Regular", size: 12)
return user
}()
private let voiceVolumeImageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
private func activateConstraints() {
updateOwnerImageConstraints()
voiceVolumeImageView.snp.remakeConstraints { make in
make.leading.equalTo(homeOwnerImageView.snp.trailing).offset(6.scale375())
make.width.height.equalTo(14)
make.centerY.equalToSuperview()
}
userNameLabel.snp.makeConstraints { make in
make.leading.equalTo(voiceVolumeImageView.snp.trailing).offset(5)
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().offset(-8)
}
}
private func updateOwnerImageConstraints() {
guard let _ = homeOwnerImageView.superview else { return }
homeOwnerImageView.snp.remakeConstraints { make in
make.leading.equalToSuperview()
make.width.height.equalTo(isShownHomeOwnerImageView ? 24 : 0)
make.top.bottom.equalToSuperview()
}
}
}
// MARK: - Public
extension VideoSeatUserStatusView {
func updateUserStatus(_ item: VideoSeatItem) {
if !item.userName.isEmpty {
userNameLabel.text = item.userName
} else {
userNameLabel.text = item.userId
}
if item.userRole == .roomOwner {
homeOwnerImageView.image = UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil)
} else if item.userRole == .administrator {
homeOwnerImageView.image = UIImage(named: "room_administrator", in: tuiRoomKitBundle(), compatibleWith: nil)
}
isShownHomeOwnerImageView = item.userRole != .generalUser
homeOwnerImageView.isHidden = !isShownHomeOwnerImageView
updateOwnerImageConstraints()
updateUserVolume(hasAudio: item.hasAudioStream, volume: item.userVoiceVolume)
}
func updateUserVolume(hasAudio: Bool, volume: Int) {
if hasAudio {
let volumeImageName = volume <= 0 ? "room_voice_volume1" : "room_voice_volume2"
voiceVolumeImageView.image = UIImage(named: volumeImageName, in: tuiRoomKitBundle(), compatibleWith: nil)
} else {
voiceVolumeImageView.image = UIImage(named: "room_mute_audio", in: tuiRoomKitBundle(), compatibleWith: nil)?.checkOverturn()
}
}
}

View File

@@ -0,0 +1,373 @@
//
// TUIVideoSeat.swift
// TUIVideoSeat
//
// Created by WesleyLei on 2022/9/13.
// Copyright © 2022 Tencent. All rights reserved.
//
import UIKit
protocol TUIVideoSeatViewResponder: AnyObject {
func switchPosition()
func clickVideoSeat()
func startPlayVideoStream(item: VideoSeatItem, renderView: UIView?)
func stopPlayVideoStream(item: VideoSeatItem)
func updateSpeakerPlayVideoState(currentPageIndex: Int)
func stopScreenCapture()
}
class TUIVideoSeatView: UIView {
private let CellID_Normal = "VideoSeatCell_Normal"
private let CellID_Mini = "VideoSeatCell_Mini"
private let viewModel: TUIVideoSeatViewModel
private var isViewReady: Bool = false
weak var responder: TUIVideoSeatViewResponder?
private var pageControl: UIPageControl = {
let control = UIPageControl()
control.currentPage = 0
control.numberOfPages = 1
control.hidesForSinglePage = true
control.isUserInteractionEnabled = false
return control
}()
init(viewModel: TUIVideoSeatViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
viewModel.viewResponder = self
responder = viewModel
isUserInteractionEnabled = true
}
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else { return }
constructViewHierarchy()
activateConstraints()
bindInteraction()
isViewReady = true
}
override func layoutSubviews() {
super.layoutSubviews()
if let item = moveMiniscreen.seatItem,!moveMiniscreen.isHidden {
moveMiniscreen.updateSize(size: videoSeatLayout.getMiniscreenFrame(item: item).size)
}
let offsetYu = Int(attendeeCollectionView.contentOffset.x) % Int(attendeeCollectionView.mm_w)
let offsetMuti = CGFloat(offsetYu) / attendeeCollectionView.mm_w
let currentPage = (offsetMuti > 0.5 ? 1 : 0) + (Int(attendeeCollectionView.contentOffset.x) / Int(attendeeCollectionView.mm_w))
attendeeCollectionView.setContentOffset(
CGPoint(x: CGFloat(pageControl.currentPage) * attendeeCollectionView.frame.size.width,
y: attendeeCollectionView.contentOffset.y), animated: false)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var videoSeatLayout: VideoSeatLayout = {
let layout = VideoSeatLayout(viewModel: viewModel)
layout.delegate = self
return layout
}()
lazy var attendeeCollectionView: UICollectionView = {
let collection = UICollectionView(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height), collectionViewLayout:
videoSeatLayout)
collection.register(VideoSeatCell.self, forCellWithReuseIdentifier: CellID_Normal)
collection.register(TUIVideoSeatDragCell.self, forCellWithReuseIdentifier: CellID_Mini)
collection.isPagingEnabled = true
collection.showsVerticalScrollIndicator = false
collection.showsHorizontalScrollIndicator = false
collection.isUserInteractionEnabled = true
collection.contentMode = .scaleToFill
collection.backgroundColor = UIColor(0x0F1014)
if #available(iOS 11.0, *) {
collection.contentInsetAdjustmentBehavior = .never
} else {
// Fallback on earlier versions
}
if #available(iOS 10.0, *) {
collection.isPrefetchingEnabled = true
} else {
// Fallback on earlier versions
}
collection.dataSource = self
collection.delegate = self
return collection
}()
lazy var moveMiniscreen: TUIVideoSeatDragCell = {
let cell = TUIVideoSeatDragCell()
cell.frame = videoSeatLayout.getMiniscreenFrame(item: nil)
cell.isHidden = true
addSubview(cell)
return cell
}()
lazy var screenCaptureMaskView: ScreenCaptureMaskView = {
let view = ScreenCaptureMaskView(frameType: .fullScreen)
view.responder = self.responder
view.isHidden = true
return view
}()
let placeholderView: UIView = {
let view = UIView(frame: .zero)
view.isHidden = true
return view
}()
func constructViewHierarchy() {
backgroundColor = .clear
addSubview(placeholderView)
addSubview(attendeeCollectionView)
addSubview(pageControl)
addSubview(screenCaptureMaskView)
}
func activateConstraints() {
placeholderView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
attendeeCollectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
pageControl.snp.makeConstraints { make in
make.height.equalTo(24)
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-5)
}
screenCaptureMaskView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
func bindInteraction() {
screenCaptureMaskView.isHidden = !EngineManager.shared.store.currentUser.hasScreenStream
addGesture()
}
private func addGesture() {
let tap = UITapGestureRecognizer(target: self, action: #selector(clickVideoSeat))
addGestureRecognizer(tap)
}
@objc private func clickVideoSeat() {
responder?.clickVideoSeat()
}
func updatePageControl() {
let offsetYu = Int(attendeeCollectionView.contentOffset.x) % Int(attendeeCollectionView.mm_w)
let offsetMuti = CGFloat(offsetYu) / attendeeCollectionView.mm_w
pageControl.currentPage = (offsetMuti > 0.5 ? 1 : 0) + (Int(attendeeCollectionView.contentOffset.x) / Int(attendeeCollectionView.mm_w))
if let seatItem = moveMiniscreen.seatItem, seatItem.hasVideoStream {
if pageControl.currentPage == 0 && !moveMiniscreen.isHidden {
responder?.startPlayVideoStream(item: seatItem, renderView: moveMiniscreen.renderView)
} else {
responder?.startPlayVideoStream(item: seatItem, renderView: getVideoVisibleCell(seatItem)?.renderView)
}
}
}
deinit {
debugPrint("deinit \(self)")
}
}
// MARK: - TUIVideoSeatViewModelResponder
extension TUIVideoSeatView: TUIVideoSeatViewModelResponder {
private func freshCollectionView(block: () -> Void) {
CATransaction.begin()
CATransaction.setDisableActions(true)
block()
CATransaction.commit()
}
func reloadData() {
freshCollectionView { [weak self] in
guard let self = self else { return }
self.attendeeCollectionView.reloadData()
}
}
func insertItems(at indexPaths: [IndexPath]) {
freshCollectionView { [weak self] in
guard let self = self else { return }
let cellNumber = self.attendeeCollectionView.numberOfItems(inSection: 0)
let listSeatItemNumber = self.viewModel.listSeatItem.count
guard cellNumber + indexPaths.count == listSeatItemNumber else { return }
self.attendeeCollectionView.performBatchUpdates { [weak self] in
guard let self = self else { return }
self.attendeeCollectionView.insertItems(at: indexPaths)
}
}
}
func deleteItems(at indexPaths: [IndexPath]) {
freshCollectionView { [weak self] in
guard let self = self else { return }
var resultArray: [IndexPath] = []
let numberOfSections = self.attendeeCollectionView.numberOfSections
for indexPath in indexPaths {
let section = indexPath.section
let item = indexPath.item
guard section < numberOfSections && item < self.attendeeCollectionView.numberOfItems(inSection: section)
else { continue }
resultArray.append(indexPath)
}
let cellNumber = self.attendeeCollectionView.numberOfItems(inSection: 0)
let listSeatItemNumber = self.viewModel.listSeatItem.count
guard cellNumber - indexPaths.count == listSeatItemNumber else { return }
self.attendeeCollectionView.performBatchUpdates { [weak self] in
guard let self = self else { return }
self.attendeeCollectionView.deleteItems(at: resultArray)
}
}
}
func updateVideoSeatCellUI(_ item: VideoSeatItem) {
if let seatItem = moveMiniscreen.seatItem, seatItem.userId == item.userId {
moveMiniscreen.updateUI(item: seatItem)
}
guard let cell = getVideoVisibleCell(item) else { return }
cell.updateUI(item: item)
}
func updateSeatVolume(_ item: VideoSeatItem) {
guard let cell = getVideoVisibleCell(item) else { return }
cell.updateUIVolume(item: item)
}
func getVideoVisibleCell(_ item: VideoSeatItem) -> VideoSeatCell? {
let cellArray = attendeeCollectionView.visibleCells
guard let cell = cellArray.first(where: { cell in
if let seatCell = cell as? VideoSeatCell, seatCell.seatItem == item {
return true
} else {
return false
}
}) as? VideoSeatCell else { return nil }
return cell
}
func updateMiniscreen(_ item: VideoSeatItem?) {
guard let item = item else {
moveMiniscreen.isHidden = true
return
}
if attendeeCollectionView.contentOffset.x > 0 {
return
}
if let seatItem = moveMiniscreen.seatItem, seatItem.userId != item.userId, (getVideoVisibleCell(seatItem) == nil) {
responder?.stopPlayVideoStream(item: seatItem)
}
moveMiniscreen.updateSize(size: videoSeatLayout.getMiniscreenFrame(item: item).size)
moveMiniscreen.isHidden = false
bringSubviewToFront(moveMiniscreen)
moveMiniscreen.updateUI(item: item)
if item.isHasVideoStream {
responder?.startPlayVideoStream(item: item, renderView: moveMiniscreen.renderView)
}
}
func updateMiniscreenVolume(_ item: VideoSeatItem) {
moveMiniscreen.updateUIVolume(item: item)
}
func getMoveMiniscreen() -> TUIVideoSeatDragCell {
return moveMiniscreen
}
func showScreenCaptureMaskView(isShow: Bool) {
screenCaptureMaskView.isHidden = !isShow
if isShow {
screenCaptureMaskView.superview?.bringSubviewToFront(screenCaptureMaskView)
}
}
func destroyVideoSeatResponder() {
responder = nil
attendeeCollectionView.delegate = nil
attendeeCollectionView.dataSource = nil
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension TUIVideoSeatView: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let seatItem = viewModel.listSeatItem[safe: indexPath.item] else { return }
guard let seatCell = cell as? VideoSeatCell else { return }
if seatItem.isHasVideoStream {
responder?.startPlayVideoStream(item: seatItem, renderView: seatCell.renderView)
} else {
responder?.stopPlayVideoStream(item: seatItem)
}
seatCell.updateUI(item: seatItem)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let seatCell = cell as? VideoSeatCell else { return }
if let seatItem = seatCell.seatItem {
responder?.stopPlayVideoStream(item: seatItem)
}
}
}
extension TUIVideoSeatView: UIScrollViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let currentPageIndex = Int(scrollView.contentOffset.x / scrollView.mm_w)
responder?.updateSpeakerPlayVideoState(currentPageIndex: currentPageIndex)
if currentPageIndex == 0 {
addSubview(moveMiniscreen)
} else {
attendeeCollectionView.addSubview(moveMiniscreen)
}
updatePageControl()
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
attendeeCollectionView.addSubview(moveMiniscreen)
}
}
// MARK: - UICollectionViewDataSource
extension TUIVideoSeatView: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.listSeatItem.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if viewModel.videoSeatViewType == .largeSmallWindowType, indexPath.row == 1 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellID_Mini, for: indexPath) as! TUIVideoSeatDragCell
cell.clickBlock = {[weak self] in
guard let self = self else { return }
self.viewModel.switchPosition()
}
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: CellID_Normal,
for: indexPath) as! VideoSeatCell
return cell
}
}
}
// MARK: - UICollectionViewDataSource
extension TUIVideoSeatView: VideoSeatLayoutDelegate {
func updateNumberOfPages(numberOfPages: NSInteger) {
pageControl.numberOfPages = numberOfPages
}
}

View File

@@ -0,0 +1,177 @@
//
// WaterMarkLayer.swift
// TUIRoomKit
//
// Created by janejntang on 2024/4/3.
//
import Foundation
class WaterMarkLayer: CALayer {
var text: String = ""
var textColor: UIColor = UIColor(0x99A2B2).withAlphaComponent(0.3)
var lineStyle: WaterMarkLineStyle = .multiLine
private var portraitImage: UIImage?
private var landscapeImage: UIImage?
private let multiLineTextFontSize = 14.0
private let sigleLineTextFontSize = 36.0
private let singleLineWaterMarkWidth = 303.scale375()
private let multiLineWaterMarkWidth = 118.scale375()
private let offset = 39.scale375()
private var numberOfOneRow: Int {
isLandscape ? 4 : 3
}
private var numberOfOneColumn: Int {
isLandscape ? 3 : 4
}
private var textMinOffset: CGFloat {
return lineStyle == .multiLine ? 2 : 4
}
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
let rect:CGRect = .init(origin: .zero, size: CGSize(width: ctx.width, height: ctx.height))
ctx.translateBy(x: rect.origin.x, y: rect.origin.y)
ctx.translateBy(x: 0, y: rect.size.height)
ctx.scaleBy(x: 1.0, y: -1.0)
ctx.translateBy(x: -rect.origin.x, y: -rect.origin.y)
let waterMarkFullSize = CGSize(width: CGFloat(ctx.width) + offset * 2, height: CGFloat(ctx.height) + offset * 2)
guard let image = getWaterMarkImage(isLandScape: isLandscape, andFullSize: waterMarkFullSize)?.cgImage
else { return }
ctx.draw(image, in: CGRect(origin: CGPoint(x: -offset, y: -offset), size: waterMarkFullSize))
}
private func getWaterMarkImage(isLandScape: Bool, andFullSize fsize: CGSize) -> UIImage? {
var image: UIImage?
if isLandscape {
image = landscapeImage != nil ? landscapeImage : createWatermarkImage(Text: text, andFullSize: fsize)
landscapeImage = image
} else {
image = portraitImage != nil ? portraitImage : createWatermarkImage(Text: text, andFullSize: fsize)
portraitImage = image
}
return image
}
private func createWatermarkImage(Text strTxt:String, andFullSize fsize:CGSize) -> UIImage {
let attributedString = getTextAttributeString(text: text)
let _size = getTextWaterMarkSize()
if UIScreen.main.scale > 1.5 {
UIGraphicsBeginImageContextWithOptions(_size,false,0)
}
else{
UIGraphicsBeginImageContext(_size)
}
//Picture tilt
var context = UIGraphicsGetCurrentContext()
context?.concatenate(.init(translationX: _size.width * 0.8, y: _size.height * 0.4))
context?.concatenate(.init(rotationAngle: -0.25 * .pi))
context?.concatenate(.init(translationX: -_size.width * 0.8, y: -_size.height * 0.4))
let point = getTextWaterMarkPoint(attributedString: attributedString, size: _size)
attributedString.draw(in: .init(origin: point, size: attributedString.size()))
let _waterImg = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage.init()
if UIScreen.main.scale > 1.5 {
UIGraphicsBeginImageContextWithOptions(fsize,false,0)
}
else{
UIGraphicsBeginImageContext(fsize)
}
context = UIGraphicsGetCurrentContext()
if lineStyle == .singleLine {
let _rect:CGRect = .init(origin: .init(x: (fsize.width - _size.width) / 2.0,
y: (fsize.height - _size.height) / 2.0),
size: _waterImg.size)
_waterImg.draw(in: _rect)
} else {
var _tempC = fsize.width / _waterImg.size.width
var _maxColumn:Int = _tempC.isNaN || !_tempC.isFinite ? 1 : Int(_tempC)
if fsize.width.truncatingRemainder(dividingBy: _waterImg.size.width) != 0 {
_maxColumn += 1
}
_tempC = fsize.height / _waterImg.size.height
var _maxRows:Int = _tempC.isNaN || !_tempC.isFinite ? 1 : Int(_tempC)
if fsize.height.truncatingRemainder(dividingBy: _waterImg.size.height) != 0 {
_maxRows += 1
}
let spaceX = (fsize.width - multiLineWaterMarkWidth * CGFloat(numberOfOneRow)) / CGFloat(numberOfOneRow - 1)
let spaceY = (fsize.height - multiLineWaterMarkWidth * CGFloat(numberOfOneColumn)) / CGFloat(numberOfOneColumn - 1)
for r in 0..<_maxRows {
for c in 0..<_maxColumn {
let _rect:CGRect = .init(origin: .init(x: CGFloat(c) * CGFloat(_waterImg.size.width + spaceX),
y: CGFloat(r) * CGFloat(_waterImg.size.height + spaceY)),
size: _waterImg.size)
_waterImg.draw(in: _rect)
}
}
}
context?.clip()
context?.setFillColor(UIColor.clear.cgColor)
context?.fill(.init(origin: .zero, size: fsize))
let _canvasImg = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage.init()
UIGraphicsEndImageContext()
return _canvasImg
}
private func getTextAttributeString(text: String) -> NSMutableAttributedString {
let textFont: CGFloat = lineStyle == .multiLine ? multiLineTextFontSize : sigleLineTextFontSize
let paragraphStyle:NSMutableParagraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byWordWrapping
paragraphStyle.lineSpacing = 5
paragraphStyle.alignment = .center
var textAttributes:[NSAttributedString.Key:Any] = [
.font : UIFont.systemFont(ofSize: textFont, weight: .regular),
.foregroundColor:textColor,
.paragraphStyle: paragraphStyle,
.kern:1.0,
]
if #available(iOS 14.0, *) {
textAttributes[.tracking] = 1.0
}
let attributedString:NSMutableAttributedString = NSMutableAttributedString.init(string: text)
let stringRange = NSMakeRange(0, attributedString.string.utf16.count)
attributedString.addAttributes(textAttributes,range: stringRange)
let viewSize = getTextWaterMarkSize()
let maxLength = getViewHypotenuseLength(viewSize: viewSize) - textMinOffset * 2
if attributedString.size().width > maxLength, let range = text.range(of: "(") {
var wartMarkText = text
let location = wartMarkText.distance(from: wartMarkText.startIndex, to: range.lowerBound)
let index = wartMarkText.index(wartMarkText.startIndex, offsetBy: location)
wartMarkText.insert(contentsOf: "\n", at: index)
attributedString.replaceCharacters(in: stringRange, with: wartMarkText)
}
return attributedString
}
private func getTextWaterMarkSize() -> CGSize {
switch lineStyle {
case .singleLine:
return CGSize(width: singleLineWaterMarkWidth, height: singleLineWaterMarkWidth)
case .multiLine:
return CGSize(width: multiLineWaterMarkWidth, height: multiLineWaterMarkWidth)
}
}
private func getTextWaterMarkPoint(attributedString: NSMutableAttributedString, size: CGSize) -> CGPoint {
let viewHypotenuseLength = getViewHypotenuseLength(viewSize: size)
var value = (viewHypotenuseLength - attributedString.size().width) / 2.0
value = max(value, textMinOffset)
return CGPoint(x: value, y: value)
}
private func getViewHypotenuseLength(viewSize: CGSize) -> CGFloat {
let square = viewSize.width * viewSize.width + viewSize.height + viewSize.height
return sqrt(square)
}
}

View File

@@ -0,0 +1,13 @@
//
// WaterMarkLineStyle.swift
// TUIRoomKit
//
// Created by janejntang on 2024/4/7.
//
import Foundation
enum WaterMarkLineStyle {
case singleLine
case multiLine
}

View File

@@ -0,0 +1,806 @@
//
// BottomViewModel.swift
// Alamofire
//
// Created by aby on 2022/12/22.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
import TUICore
import Factory
import Combine
protocol BottomViewModelResponder: AnyObject {
func updateButtonView(item: ButtonItemData)
func makeToast(text: String)
func updataBottomView(isUp:Bool)
func updateStackView(items: [ButtonItemData])
func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?)
}
class BottomViewModel: NSObject {
private(set) var viewItems: [ButtonItemData] = []
weak var viewResponder: BottomViewModelResponder?
var engineManager: EngineManager {
EngineManager.shared
}
var engineEventCenter: EngineEventCenter {
EngineEventCenter.shared
}
var roomInfo: TUIRoomInfo {
engineManager.store.roomInfo
}
var currentUser: UserEntity {
engineManager.store.currentUser
}
var attendeeList: [UserEntity] {
engineManager.store.attendeeList
}
var inviteSeatList: [RequestEntity] {
engineManager.store.inviteSeatList
}
var isCalledFromShareScreen = false
var cancellableSet = Set<AnyCancellable>()
private lazy var memberItem: ButtonItemData = {
let memberItem = ButtonItemData()
memberItem.normalTitle = String(format: .memberText, attendeeList.count)
memberItem.normalIcon = "room_member"
memberItem.resourceBundle = tuiRoomKitBundle()
memberItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.memberAction(sender: button)
}
return memberItem
}()
private lazy var muteAudioItem: ButtonItemData = {
let muteAudioItem = ButtonItemData()
muteAudioItem.normalTitle = .muteAudioText
muteAudioItem.selectedTitle = .unMuteAudioText
muteAudioItem.normalIcon = "room_unMute_audio"
muteAudioItem.selectedIcon = "room_mic_off"
muteAudioItem.resourceBundle = tuiRoomKitBundle()
muteAudioItem.buttonType = .muteAudioItemType
muteAudioItem.isSelect = !currentUser.hasAudioStream
muteAudioItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.muteAudioAction(sender: button)
}
return muteAudioItem
}()
private lazy var muteVideoItem: ButtonItemData = {
let muteVideoItem = ButtonItemData()
muteVideoItem.normalTitle = .muteVideoText
muteVideoItem.selectedTitle = .unMuteVideoText
muteVideoItem.normalIcon = "room_camera_on"
muteVideoItem.selectedIcon = "room_camera_off"
muteVideoItem.resourceBundle = tuiRoomKitBundle()
muteVideoItem.buttonType = .muteVideoItemType
muteVideoItem.isSelect = !currentUser.hasVideoStream
muteVideoItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.muteVideoAction(sender: button)
}
return muteVideoItem
}()
private lazy var shareScreenItem: ButtonItemData = {
let shareScreenItem = ButtonItemData()
shareScreenItem.normalTitle = .shareScreenOnText
shareScreenItem.selectedTitle = .shareScreenOffText
shareScreenItem.normalIcon = "room_shareScreen_on"
shareScreenItem.selectedIcon = "room_shareScreen_off"
shareScreenItem.resourceBundle = tuiRoomKitBundle()
shareScreenItem.buttonType = .shareScreenItemType
shareScreenItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.shareScreenAction(sender: button)
}
return shareScreenItem
}()
private lazy var chatItem: ButtonItemData = {
let chatItem = ButtonItemData()
chatItem.normalIcon = "room_chat"
chatItem.normalTitle = .chatText
chatItem.resourceBundle = tuiRoomKitBundle()
chatItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.chatAction(sender: button)
}
return chatItem
}()
private lazy var moreItem: ButtonItemData = {
let moreItem = ButtonItemData()
moreItem.normalTitle = .unfoldText
moreItem.normalIcon = "room_more"
moreItem.selectedTitle = .dropText
moreItem.selectedIcon = "room_drop"
moreItem.resourceBundle = tuiRoomKitBundle()
moreItem.buttonType = .moreItemType
moreItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.moreAction(sender: button)
}
return moreItem
}()
private lazy var floatItem: ButtonItemData = {
let floatItem = ButtonItemData()
floatItem.normalTitle = .floatText
floatItem.normalIcon = "room_float"
floatItem.resourceBundle = tuiRoomKitBundle()
floatItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.floatAction(sender: button)
}
return floatItem
}()
private lazy var setupItem: ButtonItemData = {
let setupItem = ButtonItemData()
setupItem.normalTitle = .setupText
setupItem.normalIcon = "room_setting"
setupItem.resourceBundle = tuiRoomKitBundle()
setupItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.setupAction(sender: button)
}
return setupItem
}()
private lazy var inviteItem: ButtonItemData = {
let inviteItem = ButtonItemData()
inviteItem.normalTitle = .inviteText
inviteItem.normalIcon = "room_invite"
inviteItem.resourceBundle = tuiRoomKitBundle()
inviteItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.inviteAction(sender: button)
}
return inviteItem
}()
private lazy var raiseHandApplyItem: ButtonItemData = {
let item = ButtonItemData()
item.normalTitle = .stageManagementText
item.normalIcon = "room_hand_raise_list"
item.resourceBundle = tuiRoomKitBundle()
item.buttonType = .raiseHandApplyItemType
item.noticeText = String(inviteSeatList.count)
item.hasNotice = inviteSeatList.count > 0
item.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.raiseHandApplyAction(sender: button)
}
return item
}()
private lazy var raiseHandItem: ButtonItemData = {
let item = ButtonItemData()
item.normalTitle = currentUser.userRole == .generalUser ? .applyJoinStageText : .joinStageText
item.normalIcon = "room_apply_join_stage"
item.selectedIcon = "room_cancel_request"
item.selectedTitle = .cancelStageText
item.resourceBundle = tuiRoomKitBundle()
item.buttonType = .raiseHandItemType
item.isSelect = engineManager.store.selfTakeSeatRequestId != nil
item.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.raiseHandAction(sender: button)
}
return item
}()
private lazy var leaveSeatHandItem: ButtonItemData = {
let item = ButtonItemData()
item.normalIcon = "room_leave_seat"
item.selectedIcon = "room_apply_join_stage"
item.normalTitle = .leaveSeatText
item.selectedTitle = .applyJoinStageText
item.buttonType = .leaveSeatItemType
item.resourceBundle = tuiRoomKitBundle()
item.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.leaveSeatAction(sender: button)
}
return item
}()
private lazy var invitationPopupPublisher = {
conferenceStore.select(ViewSelectors.getShowinvitationPopupView)
}()
override init() {
super.init()
createBottomData()
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_UserOnSeatChanged, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasAudioStream, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasVideoStream, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_SomeoneSharing, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewUserList, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onInitialSelfUserInfo, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
NotificationCenter.default.addObserver(self,
selector: #selector(onUserScreenCaptureStarted),
name: UIScreen.capturedDidChangeNotification, object: nil)
invitationPopupPublisher
.receive(on: DispatchQueue.mainQueue)
.sink { [weak self] showInvitationPopupView in
guard let self = self else { return }
if showInvitationPopupView {
showMemberSelectViewAction()
}
}
.store(in: &cancellableSet)
}
func createBottomData() {
creatBaseBottomData()
createMoreBottomData()
}
func creatBaseBottomData(){
viewItems.append(memberItem)
viewItems.append(muteAudioItem)
viewItems.append(muteVideoItem)
if roomInfo.isSeatEnabled {
if currentUser.userRole == .roomOwner {
viewItems.append(raiseHandApplyItem)
} else {
if currentUser.isOnSeat {
viewItems.append(leaveSeatHandItem)
} else {
viewItems.append(raiseHandItem)
}
}
if currentUser.userRole == .administrator {
viewItems.append(raiseHandApplyItem)
}
}
viewItems.append(shareScreenItem)
if hasTUIChatItem() {
viewItems.append(chatItem)
}
viewItems.append(moreItem)
updateAudioItem()
updateVideoItem()
}
func createMoreBottomData(){
viewItems.append(inviteItem)
viewItems.append(floatItem)
viewItems.append(setupItem)
reorderTheMoreItem()
}
func memberAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .userListViewType, height: 720.scale375Height(), backgroundColor: UIColor(0x17181F))
}
func muteAudioAction(sender: UIButton) {
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
if currentUser.hasAudioStream {
engineManager.muteLocalAudio()
return
}
//If all hosts are muted, ordinary members of the room cannot turn on their microphones.
if self.roomInfo.isMicrophoneDisableForAllUser && currentUser.userRole == .generalUser {
viewResponder?.makeToast(text: .muteAudioRoomReasonText)
return
}
//If you are speaking in a room with your hand raised and you are not on the microphone, you cannot turn on the microphone.
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
viewResponder?.makeToast(text: .muteSeatReasonText)
return
}
engineManager.unmuteLocalAudio()
guard !engineManager.store.audioSetting.isMicOpened else { return }
engineManager.openLocalMicrophone()
}
func muteVideoAction(sender: UIButton) {
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
if currentUser.hasVideoStream {
engineManager.closeLocalCamera()
return
}
//If the entire host bans paintings, ordinary members of the room cannot turn on the camera.
if self.roomInfo.isCameraDisableForAllUser && self.currentUser.userRole == .generalUser {
viewResponder?.makeToast(text: .muteVideoRoomReasonText)
return
}
//If you are speaking in a room with your hands raised and you are not on the mic, you cannot turn on the camera.
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
viewResponder?.makeToast(text: .muteSeatReasonText)
return
}
engineManager.setLocalVideoView(streamType: .cameraStream, view: nil)
engineManager.openLocalCamera()
}
func raiseHandApplyAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .raiseHandApplicationListViewType, height: 720.scale375Height(), backgroundColor: UIColor(0x22262E))
}
func raiseHandAction(sender: UIButton) {
sender.isSelected = !sender.isSelected
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
if sender.isSelected {
handleRaiseHandAction()
} else {
handleCancelRaiseHandAction()
}
}
func handleRaiseHandAction() {
_ = engineManager.takeSeat() { [weak self] _,_ in
guard let self = self else { return }
self.viewResponder?.makeToast(text: .takenSeatText)
} onRejected: { [weak self] _, _, _ in
guard let self = self else { return }
self.viewResponder?.makeToast(text: .rejectedTakeSeatText)
self.changeItemSelectState(type: .raiseHandItemType, isSelected: false)
} onTimeout: { [weak self] requestId, userId in
guard let self = self else { return }
self.viewResponder?.makeToast(text: .joinStageApplicationTimedOutText)
self.changeItemSelectState(type: .raiseHandItemType, isSelected: false)
} onError: { [weak self] _, _, code, message in
guard let self = self else { return }
self.changeItemSelectState(type: .raiseHandItemType, isSelected: false)
}
changeItemSelectState(type: .raiseHandItemType)
guard currentUser.userRole == .generalUser else { return }
viewResponder?.makeToast(text: .applicationHasSentText)
}
func handleCancelRaiseHandAction() {
engineManager.cancelTakeSeatRequest()
changeItemSelectState(type: .raiseHandItemType)
viewResponder?.makeToast(text: .joinStageApplicationCancelledText)
}
func leaveSeatAction(sender: UIButton) {
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": true])
if currentUser.userRole == .administrator {
engineManager.leaveSeat()
} else {
viewResponder?.showAlert(title: .leaveSeatTitle, message: .leaveSeatMessage, sureTitle: .leaveSeatText, declineTitle: .toastCancelText, sureBlock: { [weak self] in
guard let self = self else { return }
self.engineManager.leaveSeat()
}, declineBlock: nil)
}
}
func shareScreenAction(sender: UIButton) {
if #available(iOS 12.0, *) {
guard let item = viewItems.first(where: { $0.buttonType == .shareScreenItemType })
else { return }
if !item.isSelect {
//If someone else is screen sharing, you can no longer screen share yourself
guard engineManager.store.attendeeList.first(where: {$0.hasScreenStream}) == nil else {
viewResponder?.makeToast(text: .othersScreenSharingText)
return
}
//If you are in a room where you are raising your hand to speak, and you are not on the mic, you cannot share your screen.
guard !(roomInfo.isSeatEnabled && !currentUser.isOnSeat) else {
viewResponder?.makeToast(text: .muteSeatReasonText)
return
}
if TUICore.callService(TUICore_PrivacyService,
method: TUICore_PrivacyService_ScreenShareAntifraudReminderMethod,
param: nil, resultCallback: { [weak self] code, message, param in
guard let self = self else { return }
if code == TUICore_PrivacyService_EnableScreenShareAntifraudReminderMethod_Continue {
self.isCalledFromShareScreen = true
BroadcastLauncher.launch()
}
}) == nil {
isCalledFromShareScreen = true
BroadcastLauncher.launch()
}
} else {
viewResponder?.showAlert(title: .toastTitleText, message: .toastMessageText, sureTitle: .toastStopText, declineTitle: .toastCancelText, sureBlock: { [weak self] in
guard let self = self else { return }
self.engineManager.stopScreenCapture()
}, declineBlock: nil)
}
} else {
viewResponder?.makeToast(text: .versionLowToastText)
}
}
func chatAction(sender: UIButton) {
let user = engineManager.store.currentUser
let roomInfo = engineManager.store.roomInfo
RoomRouter.shared.pushToChatController(user: user, roomInfo: roomInfo)
}
func moreAction(sender: UIButton) {
sender.isSelected = !sender.isSelected
engineEventCenter.notifyUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, param: ["isDelay": !sender.isSelected])
viewResponder?.updataBottomView(isUp: sender.isSelected)
changeItemSelectState(type: .moreItemType)
}
func inviteAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .inviteViewType, height: 158.scale375Height())
}
func floatAction(sender: UIButton) {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowRoomVideoFloatView, param: [:])
}
func setupAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .mediaSettingViewType, height: 709.scale375Height())
}
func showMemberSelectViewAction() {
conferenceStore.dispatch(action: InvitationViewActions.resetPopupViewFlag())
let inRoomUsers = attendeeList.map{ UserInfo(userEntity: $0).convertToUser() }
// TODO: @jeremiawang Use ConferenceRouter to push ContactVC
let participants = ConferenceParticipants(unSelectableList: inRoomUsers)
guard let vc = Container.shared.contactViewController(participants) as? (ContactViewProtocol & UIViewController) else {
return
}
vc.delegate = self
RoomRouter.shared.push(viewController: vc)
}
@objc func onUserScreenCaptureStarted(notification:Notification)
{
guard let screen = notification.object as? UIScreen else {return}
if screen.isCaptured,isCalledFromShareScreen {
engineManager.startScreenCapture()
}
}
deinit {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_UserOnSeatChanged, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasAudioStream, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasVideoStream, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_SomeoneSharing, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewUserList, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onInitialSelfUserInfo, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
NotificationCenter.default.removeObserver(self, name: UIScreen.capturedDidChangeNotification, object: nil)
debugPrint("deinit \(self)")
}
@Injected(\.conferenceStore) private var conferenceStore: ConferenceStore
}
// MARK: - Private
extension BottomViewModel {
private func hasTUIChatItem() -> Bool {
return TUICore.getService(TUICore_TUIChatService) != nil
}
private func changeItemSelectState(type: ButtonItemData.ButtonType, isSelected: Bool? = nil) {
guard let item = viewItems.first(where: { $0.buttonType == type })
else { return }
if let isSelected = isSelected {
item.isSelect = isSelected
} else {
item.isSelect = !item.isSelect
}
viewResponder?.updateButtonView(item: item)
}
private func updateRaiseHandItem() {
guard roomInfo.isSeatEnabled else { return }
raiseHandItem.normalTitle = currentUser.userRole == .generalUser ? .applyJoinStageText : .joinStageText
leaveSeatHandItem.isSelect = false
raiseHandItem.isSelect = false
if currentUser.userRole == .roomOwner {
guard let index = viewItems.firstIndex(where:{ $0.buttonType == .leaveSeatItemType || $0.buttonType == .raiseHandItemType }) else { return }
viewItems.remove(at: index)
} else if let index = viewItems.firstIndex(where:{ $0.buttonType == .leaveSeatItemType || $0.buttonType == .raiseHandItemType }) {
if currentUser.isOnSeat {
viewItems[index] = leaveSeatHandItem
} else {
viewItems[index] = raiseHandItem
}
} else {
if currentUser.isOnSeat {
addViewItem(buttonItem: leaveSeatHandItem, index: 3)
} else {
addViewItem(buttonItem: raiseHandItem, index: 1)
}
}
}
private func reorderTheMoreItem() {
guard viewItems.count > 6 else { return }
guard let index = viewItems.firstIndex(where: { $0.buttonType == .moreItemType }), index != 5 else { return }
viewItems.remove(at: index)
viewItems.insert(moreItem, at: 5)
}
private func removeViewItem(buttonType: ButtonItemData.ButtonType) {
viewItems.removeAll(where: { $0.buttonType == buttonType })
}
private func addViewItem(buttonItem: ButtonItemData, index: Int) {
guard !isContainedViewItem(buttonType: buttonItem.buttonType) else { return }
if viewItems.count > index + 1 {
viewItems.insert(buttonItem, at: index)
} else {
viewItems.append(buttonItem)
}
}
private func isContainedViewItem(buttonType: ButtonItemData.ButtonType) -> Bool {
return viewItems.contains(where: { $0.buttonType == buttonType })
}
private func updateAudioItem() {
if roomInfo.isSeatEnabled, currentUser.userRole == .generalUser, !currentUser.isOnSeat {
//If the audience in the room who raises their hand to speak is not on the microphone, the microphone button will not be displayed.
removeViewItem(buttonType: .muteAudioItemType)
} else if !isContainedViewItem(buttonType: .muteAudioItemType) {
addViewItem(buttonItem: muteAudioItem, index: 1)
}
muteAudioItem.isSelect = !currentUser.hasAudioStream
muteAudioItem.alpha = checkMicAuthority() || currentUser.hasAudioStream ? 1 : 0.5
}
private func updateVideoItem() {
if roomInfo.isSeatEnabled, currentUser.userRole == .generalUser, !currentUser.isOnSeat {
removeViewItem(buttonType: .muteVideoItemType)
} else if !isContainedViewItem(buttonType: .muteVideoItemType) {
addViewItem(buttonItem: muteVideoItem, index: 2)
}
muteVideoItem.isSelect = !currentUser.hasVideoStream
muteVideoItem.alpha = checkCameraAuthority() || currentUser.hasVideoStream ? 1 : 0.5
}
private func checkMicAuthority() -> Bool {
if self.roomInfo.isMicrophoneDisableForAllUser && currentUser.userRole == .generalUser {
return false
}
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
return false
}
return true
}
private func checkCameraAuthority() -> Bool {
if self.roomInfo.isCameraDisableForAllUser && currentUser.userRole == .generalUser {
return false
}
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
return false
}
return true
}
private func updateRaiseHandApplyItem() {
guard roomInfo.isSeatEnabled else { return }
raiseHandItem.normalTitle = currentUser.userRole == .generalUser ? .applyJoinStageText : .joinStageText
if currentUser.userRole == .roomOwner {
addViewItem(buttonItem: raiseHandApplyItem, index: 3)
} else if currentUser.userRole == .administrator {
addViewItem(buttonItem: raiseHandApplyItem, index: 4)
} else {
removeViewItem(buttonType: .raiseHandApplyItemType)
}
}
}
extension BottomViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_UserOnSeatChanged:
guard roomInfo.isSeatEnabled else { return }
updateRaiseHandItem()
updateAudioItem()
updateVideoItem()
reorderTheMoreItem()
viewResponder?.updateStackView(items: viewItems)
case .TUIRoomKitService_CurrentUserRoleChanged:
updateAudioItem()
updateVideoItem()
updateRaiseHandApplyItem()
updateRaiseHandItem()
reorderTheMoreItem()
viewResponder?.updateStackView(items: viewItems)
case .TUIRoomKitService_CurrentUserHasAudioStream:
guard let hasAudio = info?["hasAudio"] as? Bool else { return }
guard let reason = info?["reason"] as? TUIChangeReason else { return }
if !hasAudio, reason == .byAdmin, !roomInfo.isMicrophoneDisableForAllUser {
if !roomInfo.isSeatEnabled {
viewResponder?.makeToast(text: .noticeMicrophoneOffTitleText)
} else if currentUser.isOnSeat {
viewResponder?.makeToast(text: .noticeMicrophoneOffTitleText)
}
}
updateAudioItem()
viewResponder?.updateButtonView(item: muteAudioItem)
case .TUIRoomKitService_CurrentUserHasVideoStream:
guard let hasVideo = info?["hasVideo"] as? Bool else { return }
guard let reason = info?["reason"] as? TUIChangeReason else { return }
if !hasVideo, reason == .byAdmin, !roomInfo.isCameraDisableForAllUser {
if !roomInfo.isSeatEnabled {
viewResponder?.makeToast(text: .noticeCameraOffTitleText)
} else if currentUser.isOnSeat {
viewResponder?.makeToast(text: .noticeCameraOffTitleText)
}
}
updateVideoItem()
viewResponder?.updateButtonView(item: muteVideoItem)
case .TUIRoomKitService_SomeoneSharing:
guard let userId = info?["userId"] as? String else { return }
guard let hasVideo = info?["hasVideo"] as? Bool else { return }
guard userId == currentUser.userId else { return }
changeItemSelectState(type: .shareScreenItemType, isSelected: hasVideo)
if !hasVideo {
isCalledFromShareScreen = false
}
case .TUIRoomKitService_RenewUserList:
memberItem.normalTitle = String(format: .memberText, attendeeList.count)
viewResponder?.updateButtonView(item: memberItem)
case .TUIRoomKitService_RenewSeatList:
raiseHandApplyItem.noticeText = String(inviteSeatList.count)
raiseHandApplyItem.hasNotice = inviteSeatList.count > 0
viewResponder?.updateButtonView(item: raiseHandApplyItem)
default: break
}
}
}
extension BottomViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onAllUserCameraDisableChanged:
updateVideoItem()
viewResponder?.updateButtonView(item: muteVideoItem)
case .onAllUserMicrophoneDisableChanged:
updateAudioItem()
viewResponder?.updateButtonView(item: muteAudioItem)
case .onInitialSelfUserInfo:
updateAudioItem()
updateVideoItem()
updateRaiseHandApplyItem()
updateRaiseHandItem()
reorderTheMoreItem()
viewResponder?.updateStackView(items: viewItems)
case .onRemoteUserEnterRoom:
guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
conferenceStore.dispatch(action: ConferenceInvitationActions.removeInvitation(payload: userInfo.userId))
default: break
}
}
}
extension BottomViewModel: ContactViewSelectDelegate {
func onMemberSelected(_ viewController: any ContactViewProtocol, invitees: [User]) {
let userIdList = invitees.map{ $0.userId }
self.conferenceStore.dispatch(action: ConferenceInvitationActions.inviteUsers(payload: (roomInfo.roomId, userIdList)))
RoomRouter.shared.pop()
if !invitees.isEmpty{
viewResponder?.makeToast(text: .inviteEnterRoomSuccesstext)
}
}
}
private extension String {
static var memberText: String {
localized("Users(%lld)")
}
static var muteAudioText: String {
localized("Mute")
}
static var unMuteAudioText: String {
localized("Unmute")
}
static var muteVideoText: String {
localized("Stop video")
}
static var unMuteVideoText: String {
localized("Start video")
}
static var stageManagementText: String {
localized("Applies")
}
static var cancelStageText: String {
localized("Cancel")
}
static var applyJoinStageText: String {
localized("Join stage")
}
static var leaveSeatText: String {
localized("Step down")
}
static var muteSeatReasonText: String {
localized("Can be turned on after taking the stage")
}
static var muteAudioRoomReasonText: String {
localized("All on mute audio, unable to turn on microphone")
}
static var muteVideoRoomReasonText: String {
localized("All on mute video, unable to turn on camera")
}
static var noticeCameraOffTitleText: String {
localized("The conference owner disabled your video.")
}
static var noticeMicrophoneOffTitleText: String {
localized("You were muted by the host.")
}
static var shareScreenOnText: String {
localized("Share")
}
static var shareScreenOffText: String {
localized("Stop")
}
static var versionLowToastText: String {
localized("Your system version is below 12.0. Please update.")
}
static var chatText: String {
localized("Chat")
}
static var unfoldText: String {
localized("More")
}
static var inviteText: String {
localized("Invite")
}
static var floatText: String {
localized("Floating")
}
static var setupText: String {
localized("Settings")
}
static var dropText: String {
localized("Drop")
}
static var rejectedTakeSeatText: String {
localized("Application to go on stage was rejected")
}
static var takenSeatText: String {
localized("Succeed on stage")
}
static var othersScreenSharingText: String {
localized("An existing member is sharing. Please try again later")
}
static var toastTitleText: String {
localized("Share Screen")
}
static var toastMessageText: String {
localized("Stop TUIRoom screen sharing screen live?")
}
static var toastCancelText: String {
localized("Cancel")
}
static var toastStopText: String {
localized("Stop")
}
static var applicationHasSentText: String {
localized("Application has been sent, please wait for the owner/administrator to approve")
}
static var joinStageText: String {
localized("Join stage")
}
static var leaveSeatTitle: String {
localized("Are you sure you want to step down?")
}
static var leaveSeatMessage: String {
localized("To get on stage again, you need to resend the application and wait for the owner/administrator to approve it.")
}
static var joinStageApplicationCancelledText: String {
localized("Application for stage has been cancelled")
}
static var joinStageApplicationTimedOutText: String {
localized("The request to go on stage has timed out")
}
static var inviteEnterRoomSuccesstext: String {
localized("Invitation has been sent, waiting for users to join")
}
}

View File

@@ -0,0 +1,653 @@
//
// ConferenceMainViewModel.swift
// TUIRoomKit
//
// Created by aby on 2022/12/27.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import TUICore
import RTCRoomEngine
import Factory
protocol ConferenceMainViewResponder: AnyObject {
func makeToast(text: String)
func changeToolBarHiddenState()
func setToolBarDelayHidden(isDelay: Bool)
func showExitRoomView()
func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?)
func showAlertWithAutoConfirm(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?, autoConfirmSeconds: Int?)
func showRaiseHandNoticeView()
func updateRoomInfo(roomInfo: TUIRoomInfo)
func showPasswordView(roomId: String)
func hidePasswordView()
func showRepeatJoinRoomAlert()
}
class ConferenceMainViewModel: NSObject {
weak var viewResponder: ConferenceMainViewResponder? = nil
var engineManager: EngineManager {
EngineManager.shared
}
var store: RoomStore {
engineManager.store
}
var roomInfo: TUIRoomInfo {
store.roomInfo
}
var currentUser: UserEntity {
store.currentUser
}
let roomRouter: RoomRouter = RoomRouter.shared
private var isShownOpenCameraInviteAlert = false
private var isShownOpenMicrophoneInviteAlert = false
private var isShownTakeSeatInviteAlert = false
private weak var localAudioViewModel: LocalAudioViewModel?
private var selfRole: TUIRole?
var joinConferenceParams: JoinConferenceParams?
var startConferenceParams: StartConferenceParams?
var isShownWaterMark: Bool = ConferenceSession.sharedInstance.implementation.isEnableWaterMark;
override init() {
super.init()
selfRole = currentUser.userRole
subscribeEngine()
subLogoutNotification()
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onRoomDismissed, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOutOfRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRequestReceived, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOffSeat, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOffLine, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserMuteMessage, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_ShowExitRoomView, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
EngineEventCenter.shared.subscribeEngine(event: .onStartedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onJoinedRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onGetUserListFinished, observer: self)
}
private func subLogoutNotification() {
NotificationCenter.default.addObserver(self,
selector: #selector(dismissConferenceViewForLogout),
name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
private func unsubLogoutNotification() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onRoomDismissed, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOutOfRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRequestReceived, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserMicrophoneDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onAllUserCameraDisableChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOffSeat, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOffLine, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserMuteMessage, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_SetToolBarDelayHidden, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_ShowExitRoomView, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onStartedRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onJoinedRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onGetUserListFinished, observer: self)
}
func hideLocalAudioView() {
localAudioViewModel?.hideLocalAudioView()
}
func showLocalAudioView() {
localAudioViewModel?.showLocalAudioView()
}
func onViewDidLoadAction() {
if store.isEnteredRoom {
let roomId = startConferenceParams?.roomId ?? joinConferenceParams?.roomId
if let roomId = roomId, store.roomInfo.roomId != roomId {
viewResponder?.showRepeatJoinRoomAlert()
}
return
}
if startConferenceParams != nil {
quickStartConference()
return
}
if joinConferenceParams != nil {
joinConference()
}
}
func quickStartConference() {
guard let startParams = startConferenceParams, !startParams.roomId.isEmpty else {
return
}
ConferenceOptions.quickStart(startConferenceParams: startParams) { [weak self] roomInfo in
guard let self = self else { return }
guard !self.viewStore.isInternalCreation else { return }
self.notifySuccess(roomInfo: roomInfo, event: .onStartedRoom)
} onError: { [weak self] code, message in
guard let self = self else { return }
self.handleOperateConferenceFailedResult(roomId: startParams.roomId, event: .onStartedRoom, error: code, message: message)
}
}
func joinConference() {
guard let joinParams = joinConferenceParams, !joinParams.roomId.isEmpty else {
return
}
ConferenceOptions.join(joinConferenParams: joinParams) { [weak self] roomInfo in
guard let self = self else { return }
self.viewResponder?.hidePasswordView()
self.notifySuccess(roomInfo: roomInfo, event: .onJoinedRoom)
} onError: { [weak self] code, message in
guard let self = self else { return }
if code == .needPassword {
self.viewResponder?.showPasswordView(roomId: joinParams.roomId)
} else if code == .wrongPassword {
self.viewResponder?.makeToast(text: .wrongPasswordText)
} else {
self.handleOperateConferenceFailedResult(roomId: joinParams.roomId, event: .onJoinedRoom, error: code, message: message)
}
}
}
func notifySuccess(roomInfo: TUIRoomInfo?,
event: EngineEventCenter.RoomEngineEvent) {
let param = [
"roomInfo" : roomInfo ?? TUIRoomInfo(),
"error" : TUIError.success,
"mesasge" : ""
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: event, param: param)
}
func notifyError(roomId: String,
event: EngineEventCenter.RoomEngineEvent,
error: TUIError,
message: String) {
let roomInfo = TUIRoomInfo()
roomInfo.roomId = roomId
let param = [
"roomInfo" : roomInfo,
"error" : error,
"mesasge" : message
] as [String : Any]
EngineEventCenter.shared.notifyEngineEvent(event: event, param: param)
}
func setJoinConferenceParams(params: JoinConferenceParams) {
joinConferenceParams = params
store.setCameraOpened(params.isOpenCamera)
store.setSoundOnSpeaker(params.isOpenSpeaker)
}
func setStartConferenceParams(params: StartConferenceParams) {
startConferenceParams = params
store.setCameraOpened(params.isOpenCamera)
store.setSoundOnSpeaker(params.isOpenSpeaker)
}
@objc func dismissConferenceViewForLogout() {
viewResponder?.showAlertWithAutoConfirm(title: .logoutText, message: nil, sureTitle: .alertOkText, declineTitle: nil, sureBlock: {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}, declineBlock: nil, autoConfirmSeconds: 5)
}
func handleWrongPasswordFault(roomId: String) {
handleOperateConferenceFailedResult(roomId: roomId, event: .onJoinedRoom, error: .wrongPassword, message: "password is wrong")
}
private func handleOperateConferenceFailedResult(roomId: String, event: EngineEventCenter.RoomEngineEvent, error: TUIError, message: String) {
if viewStore.isInternalCreation {
roomRouter.pop()
let errorText = "Error: " + String(describing: error) + ", Message: " + message
conferenceStore.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: errorText)))
} else {
notifyError(roomId: roomId, event: event, error: error, message: message)
}
}
deinit {
unsubscribeEngine()
unsubLogoutNotification()
debugPrint("deinit \(self)")
}
@Injected(\.conferenceStore) var conferenceStore: ConferenceStore
@Injected(\.conferenceMainViewStore) var viewStore: ConferenceMainViewStore
}
extension ConferenceMainViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onRoomDismissed:
handleRoomDismissed()
case .onKickedOutOfRoom:
handleKickedOutOfRoom()
case .onAllUserMicrophoneDisableChanged:
guard let isDisable = param?["isDisable"] as? Bool else { return }
handleAllUserMicrophoneDisableChanged(isDisable: isDisable)
case .onAllUserCameraDisableChanged:
guard let isDisable = param?["isDisable"] as? Bool else { return }
handleAllUserCameraDisableChanged(isDisable: isDisable)
case .onKickedOffSeat:
viewResponder?.makeToast(text: .kickedOffSeat)
case .onRequestReceived:
guard let request = param?["request"] as? TUIRequest else { return }
handleReceivedRequest(request: request)
case .onKickedOffLine:
handleKickedOffLine()
case .onStartedRoom:
guard let roomInfo = param?["roomInfo"] as? TUIRoomInfo else { return }
guard let error = param?["error"] as? TUIError else { return }
if error == .success {
handleStartRoom(roomInfo: roomInfo)
conferenceStore.dispatch(action: RoomActions.updateRoomState(payload: RoomInfo(with: roomInfo)))
}
case .onJoinedRoom:
guard let roomInfo = param?["roomInfo"] as? TUIRoomInfo else { return }
guard let error = param?["error"] as? TUIError else { return }
if error == .success {
handleJoinRoom(roomInfo: roomInfo)
conferenceStore.dispatch(action: RoomActions.updateRoomState(payload: RoomInfo(with: roomInfo)))
}
case .onGetUserListFinished:
let allUsers = self.store.attendeeList.map{ UserInfo(userEntity: $0) }
conferenceStore.dispatch(action: UserActions.updateAllUsers(payload: allUsers))
conferenceStore.dispatch(action: ConferenceInvitationActions.getInvitationList(payload: (store.roomInfo.roomId, "", [])))
default: break
}
}
private func handleRoomDismissed() {
#if RTCube_APPSTORE
if currentUser.userRole == .roomOwner {
let selector = NSSelectorFromString("showAlertUserLiveTimeOut")
if UIViewController.responds(to: selector) {
UIViewController.perform(selector)
}
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
engineManager.destroyEngineManager()
return
}
#endif
engineManager.destroyEngineManager()
viewResponder?.showAlertWithAutoConfirm(title: .destroyAlertText, message: nil, sureTitle: .alertOkText, declineTitle: nil, sureBlock: {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}, declineBlock: nil, autoConfirmSeconds: 5)
}
private func handleKickedOutOfRoom() {
engineManager.destroyEngineManager()
viewResponder?.showAlertWithAutoConfirm(title: .kickOffTitleText, message: nil, sureTitle: .alertOkText, declineTitle: nil , sureBlock: {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}, declineBlock: nil, autoConfirmSeconds: 5)
}
private func handleAllUserMicrophoneDisableChanged(isDisable: Bool) {
if isDisable {
RoomRouter.makeToastInCenter(toast: .allMuteAudioText, duration: 1.5)
} else {
RoomRouter.makeToastInCenter(toast: .allUnMuteAudioText, duration: 1.5)
}
}
private func handleAllUserCameraDisableChanged(isDisable: Bool) {
if isDisable {
RoomRouter.makeToastInCenter(toast: .allMuteVideoText, duration: 1.5)
} else {
RoomRouter.makeToastInCenter(toast: .allUnMuteVideoText, duration: 1.5)
}
}
private func handleReceivedRequest(request: TUIRequest) {
switch request.requestAction {
case .openRemoteCamera:
handleOpenCameraRequest(request: request)
case .openRemoteMicrophone:
handleOpenMicrophoneRequest(request: request)
case .invalidAction:
break
case .remoteUserOnSeat:
handleOnSeatRequest(request: request)
default: break
}
}
private func handleOpenCameraRequest(request: TUIRequest) {
guard !isShownOpenCameraInviteAlert else { return }
guard let userInfo = store.attendeeList.first(where: { $0.userId == request.userId }) else { return }
let nameText: String = userInfo.userRole == .roomOwner ? .hostText : .administratorText
let title = localizedReplace(.inviteTurnOnVideoText, replace: nameText)
viewResponder?.showAlert(title: title, message: nil, sureTitle: .agreeText, declineTitle: .declineText, sureBlock: { [weak self] in
guard let self = self else { return }
self.isShownOpenCameraInviteAlert = false
self.agreeOpenLocalCamera(request: request)
}, declineBlock: { [weak self] in
guard let self = self else { return }
self.isShownOpenCameraInviteAlert = false
self.engineManager.responseRemoteRequest(request.requestId, agree: false)
})
isShownOpenCameraInviteAlert = true
}
private func agreeOpenLocalCamera(request: TUIRequest) {
engineManager.setLocalVideoView(streamType: .cameraStream, view: nil)
if RoomCommon.checkAuthorCamaraStatusIsDenied() {
engineManager.responseRemoteRequest(request.requestId, agree: true)
} else {
RoomCommon.cameraStateActionWithPopCompletion { [weak self] granted in
guard let self = self else { return }
self.engineManager.responseRemoteRequest(request.requestId, agree: granted)
}
}
}
private func handleOpenMicrophoneRequest(request: TUIRequest) {
guard !isShownOpenMicrophoneInviteAlert else { return }
guard let userInfo = store.attendeeList.first(where: { $0.userId == request.userId }) else { return }
let nameText: String = userInfo.userRole == .roomOwner ? .hostText : .administratorText
let title = localizedReplace(.inviteTurnOnAudioText, replace: nameText)
viewResponder?.showAlert(title: title, message: nil, sureTitle: .agreeText, declineTitle: .declineText, sureBlock: { [weak self] in
guard let self = self else { return }
self.isShownOpenMicrophoneInviteAlert = false
self.agreeOpenLocalMic(request: request)
}, declineBlock: { [weak self] in
guard let self = self else { return }
self.isShownOpenMicrophoneInviteAlert = false
self.engineManager.responseRemoteRequest(request.requestId, agree: false)
})
isShownOpenMicrophoneInviteAlert = true
}
private func agreeOpenLocalMic(request: TUIRequest) {
if RoomCommon.checkAuthorMicStatusIsDenied() {
self.engineManager.responseRemoteRequest(request.requestId, agree: true)
} else {
RoomCommon.micStateActionWithPopCompletion { [weak self] granted in
guard let self = self else { return }
self.engineManager.responseRemoteRequest(request.requestId, agree: granted)
}
}
}
private func handleOnSeatRequest(request: TUIRequest) {
guard roomInfo.isSeatEnabled && !isShownTakeSeatInviteAlert else { return }
guard let userInfo = store.attendeeList.first(where: { $0.userId == request.userId }) else { return }
let nameText: String = userInfo.userRole == .roomOwner ? .hostText : .administratorText
let title = localizedReplace(.inviteSpeakOnStageTitle, replace: nameText)
viewResponder?.showAlert(title: title, message: .inviteSpeakOnStageMessage, sureTitle: .agreeSeatText, declineTitle: .declineText, sureBlock: { [weak self] in
guard let self = self else { return }
self.isShownTakeSeatInviteAlert = false
self.agreeOnSeatRequest(requestId: request.requestId)
}, declineBlock: { [weak self] in
guard let self = self else { return }
self.isShownTakeSeatInviteAlert = false
self.disagreeOnSeatRequest(requestId: request.requestId)
})
isShownTakeSeatInviteAlert = true
}
private func agreeOnSeatRequest(requestId: String) {
engineManager.responseRemoteRequest(requestId, agree: true) {
} onError: { [weak self] code, message in
guard let self = self else { return }
switch code {
case .failed:
self.viewResponder?.makeToast(text: .goOnStageTimedOutText)
case .allSeatOccupied:
self.viewResponder?.makeToast(text: .onStageNumberReachedLimitText)
default: break
}
}
}
private func disagreeOnSeatRequest(requestId: String) {
engineManager.responseRemoteRequest(requestId, agree: false) {
} onError: { code, message in
debugPrint("responseRemoteRequest:code:\(code),message:\(message)")
}
}
private func handleKickedOffLine() {
viewResponder?.showAlertWithAutoConfirm(title: .kieckedOffLineText, message: nil, sureTitle: .alertOkText, declineTitle: nil, sureBlock: {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}, declineBlock: nil, autoConfirmSeconds: 5)
}
private func handleStartRoom(roomInfo: TUIRoomInfo) {
viewResponder?.updateRoomInfo(roomInfo: roomInfo)
}
private func handleJoinRoom(roomInfo: TUIRoomInfo) {
if roomInfo.isSeatEnabled, store.isShownRaiseHandNotice {
viewResponder?.showRaiseHandNoticeView()
}
viewResponder?.updateRoomInfo(roomInfo: roomInfo)
}
}
extension ConferenceMainViewModel: ConferenceMainViewFactory {
func makeConferencePasswordView() -> ConferencePasswordView {
let passwordView = ConferencePasswordView()
passwordView.isHidden = true
passwordView.viewModel = self
return passwordView
}
func makeTopView() -> TopView {
let viewModel = TopViewModel()
let topView = TopView(viewModel: viewModel)
topView.backgroundColor = UIColor(0x0F1014)
return topView
}
func makeBottomView() -> BottomView {
let viewModel = BottomViewModel()
let bottomView = BottomView(viewModel: viewModel)
return bottomView
}
func makeVideoSeatView() -> UIView {
let viewModel = TUIVideoSeatViewModel()
let videoSeatView = TUIVideoSeatView(viewModel: viewModel)
videoSeatView.backgroundColor = UIColor(0x0F1014)
return videoSeatView
}
func makeRaiseHandNoticeView() -> UIView {
let raiseHandNoticeView = RaiseHandNoticeView()
raiseHandNoticeView.isHidden = true
return raiseHandNoticeView
}
func makeLocalAudioView() -> UIView {
let localAudioViewModel = LocalAudioViewModel()
localAudioViewModel.hideLocalAudioView()
let view = LocalAudioView(viewModel: localAudioViewModel)
self.localAudioViewModel = localAudioViewModel
return view
}
func makeWaterMarkLayer() -> WaterMarkLayer {
let layer = WaterMarkLayer()
layer.backgroundColor = UIColor.clear.cgColor
layer.anchorPoint = CGPointZero
layer.text = getWaterMarkText()
layer.lineStyle = .multiLine
layer.cornerRadius = 16
return layer
}
func makeFloatChatButton() -> FloatChatButton {
let floatchatButton = FloatChatButton()
floatchatButton.isHidden = !store.shouldShowFloatChatView
if store.isEnteredRoom {
floatchatButton.updateRoomId(roomId: store.roomInfo.roomId)
}
return floatchatButton
}
func makeFloatChatDisplayView() -> FloatChatDisplayView {
let view = FloatChatDisplayView()
view.isHidden = !store.shouldShowFloatChatView
return view
}
func makeRaiseHandApplicationNotificationView() -> RaiseHandApplicationNotificationView {
let viewModel = RaiseHandApplicationNotificationViewModel()
let notificationView = RaiseHandApplicationNotificationView(viewModel: viewModel)
return notificationView
}
private func getWaterMarkText() -> String {
let customizeText = ConferenceSession.sharedInstance.implementation.waterMarkText
if !customizeText.isEmpty {
return customizeText
}
let userId = TUILogin.getUserID() ?? currentUser.userId
let userName = TUILogin.getNickName() ?? currentUser.userName
var defaultText = userId
if !userName.isEmpty {
defaultText = defaultText + "(\(userName))"
}
return defaultText
}
}
extension ConferenceMainViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key{
case .TUIRoomKitService_CurrentUserRoleChanged:
guard let userRole = info?["userRole"] as? TUIRole else { return }
handleSelfRoleChanged(userRole: userRole)
case .TUIRoomKitService_CurrentUserMuteMessage:
guard let isMute = info?["isMute"] as? Bool else { return }
viewResponder?.makeToast(text: isMute ? .messageTurnedOffText : .messageTurnedOnText)
case .TUIRoomKitService_ChangeToolBarHiddenState:
viewResponder?.changeToolBarHiddenState()
case .TUIRoomKitService_SetToolBarDelayHidden:
guard let isDelay = info?["isDelay"] as? Bool else { return }
viewResponder?.setToolBarDelayHidden(isDelay: isDelay)
case .TUIRoomKitService_ShowExitRoomView:
viewResponder?.showExitRoomView()
case .TUIRoomKitService_DismissConferenceViewController:
conferenceStore.dispatch(action: ConferenceInvitationActions.clearInvitationList())
conferenceStore.dispatch(action: RoomActions.clearRoomState())
default: break
}
}
private func handleSelfRoleChanged(userRole: TUIRole) {
switch userRole {
case .roomOwner:
viewResponder?.makeToast(text: .haveBecomeMasterText)
case .administrator:
viewResponder?.makeToast(text: .haveBecomeAdministratorText)
case .generalUser:
if selfRole == .administrator {
viewResponder?.makeToast(text: .revokedAdministratorText)
}
default: break
}
selfRole = userRole
}
}
private extension String {
static var kickOffTitleText: String {
localized("You were removed by the host.")
}
static var destroyAlertText: String {
localized("The conference was closed.")
}
static var inviteTurnOnAudioText: String {
localized("xx invites you to turn on the microphone")
}
static var inviteTurnOnVideoText: String {
localized("xx invites you to turn on the camera")
}
static var inviteSpeakOnStageTitle: String {
localized("xx invites you to speak on stage")
}
static var inviteSpeakOnStageMessage: String {
localized("You can turn on the camera and unmute it once you are on stage")
}
static var messageTurnedOffText: String {
localized("You were muted message by the host.")
}
static var messageTurnedOnText: String {
localized("You were unmuted message by the host.")
}
static var haveBecomeMasterText: String {
localized("You are now a host")
}
static var haveBecomeAdministratorText: String {
localized("You have become a conference admin")
}
static var kickedOffLineText: String {
localized("You are already logged in elsewhere")
}
static var alertOkText: String {
localized("OK")
}
static var declineText: String {
localized("Decline")
}
static var agreeText: String {
localized("Agree")
}
static var agreeSeatText: String {
localized("Approve")
}
static var allMuteAudioText: String {
localized("All audios disabled")
}
static var allMuteVideoText: String {
localized("All videos disabled")
}
static var allUnMuteAudioText: String {
localized("All audios enabled")
}
static var allUnMuteVideoText: String {
localized("All videos enabled")
}
static var kickedOffSeat: String {
localized("You have been asked to leave stage")
}
static var hostText: String {
localized("Host")
}
static var administratorText: String {
localized("Administrator")
}
static var revokedAdministratorText: String {
localized("Your conference admin status has been revoked")
}
static var onStageNumberReachedLimitText: String {
localized("The stage is full, please contact the host")
}
static var goOnStageTimedOutText: String {
localized("Failed to go on stage, invitation has timed out")
}
static var kieckedOffLineText: String {
localized("You are already logged in elsewhere")
}
static var logoutText: String {
localized("You are logged out")
}
static let wrongPasswordText = localized("Wrong password, please re-enter")
}

View File

@@ -0,0 +1,99 @@
//
// ExitRoomViewModel.swift
// TUIRoomKit
//
// Created by krabyu on 2023/8/23.
//
import Foundation
protocol ExitRoomViewModelResponder: AnyObject {
func makeToast(message: String)
func dismissView()
}
class ExitRoomViewModel {
var engineManager: EngineManager {
EngineManager.shared
}
var isRoomOwner: Bool {
engineManager.store.currentUser.userId == engineManager.store.roomInfo.ownerId
}
weak var viewResponder: ExitRoomViewModelResponder?
func isShownLeaveRoomButton() -> Bool {
if isRoomOwner {
return getFilterRoomOwnerNumber() > 0
} else {
return true
}
}
func isShownDestroyRoomButton() -> Bool {
return isRoomOwner
}
func leaveRoomAction() {
if isRoomOwner {
if getFilterRoomOwnerNumber() == 1, let userInfo = getNextRoomOwner() {
appointMasterAndExitRoom(userId: userInfo.userId)
} else if getFilterRoomOwnerNumber() > 1 {
viewResponder?.dismissView()
RoomRouter.shared.presentPopUpViewController(viewType: .transferMasterViewType, height: 720.scale375Height())
} else {
destroyRoom()
}
} else {
exitRoom()
}
}
func exitRoom() {
engineManager.exitRoom { [weak self] in
guard let self = self else { return }
self.viewResponder?.dismissView()
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
} onError: { [weak self] code, message in
guard let self = self else { return }
self.viewResponder?.makeToast(message: message)
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}
}
func destroyRoom() {
engineManager.destroyRoom { [weak self] in
guard let self = self else { return }
self.viewResponder?.dismissView()
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
} onError: { [weak self] code, message in
guard let self = self else { return }
self.viewResponder?.makeToast(message: message)
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, param: [:])
}
}
private func getNextRoomOwner() -> UserEntity? {
let userInfoArray = engineManager.store.attendeeList.filter({ $0.userId != engineManager.store.roomInfo.ownerId })
return userInfoArray.first
}
private func getFilterRoomOwnerNumber() -> Int {
let array = engineManager.store.attendeeList.filter({ $0.userId != engineManager.store.roomInfo.ownerId })
return array.count
}
private func appointMasterAndExitRoom(userId: String) {
engineManager.changeUserRole(userId: userId, role: .roomOwner) { [weak self] in
guard let self = self else { return }
self.exitRoom()
} onError: { [weak self] code, message in
guard let self = self else { return }
self.viewResponder?.makeToast(message: message)
}
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,117 @@
//
// LocalAudioViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2024/1/5.
//
import Foundation
import RTCRoomEngine
protocol LocalAudioViewModelResponder: AnyObject {
func updateMuteAudioButton(isSelected: Bool)
func makeToast(text: String)
func show()
func hide()
}
class LocalAudioViewModel: NSObject {
weak var viewResponder: LocalAudioViewModelResponder?
var engineManager: EngineManager {
EngineManager.shared
}
var roomInfo: TUIRoomInfo {
engineManager.store.roomInfo
}
var currentUser: UserEntity {
engineManager.store.currentUser
}
var ableDisplay: Bool = false
override init() {
super.init()
subscribeUIEvent()
}
deinit {
unsubscribeUIEvent()
}
private func subscribeUIEvent() {
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasAudioStream, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_UserOnSeatChanged, responder: self)
}
private func unsubscribeUIEvent() {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserHasAudioStream, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_CurrentUserRoleChanged, responder: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_UserOnSeatChanged, responder: self)
}
func showLocalAudioView() {
ableDisplay = true
guard !checkMuteAudioHiddenState() else { return }
viewResponder?.show()
}
func hideLocalAudioView() {
ableDisplay = false
viewResponder?.hide()
}
func muteAudioAction() {
if currentUser.hasAudioStream {
engineManager.muteLocalAudio()
return
}
//If all hosts are muted, room members cannot turn on their microphones
if self.roomInfo.isMicrophoneDisableForAllUser && self.currentUser.userId != roomInfo.ownerId {
viewResponder?.makeToast(text: .muteAudioRoomReasonText)
return
}
//If you are speaking in a room with your hand raised and you are not on the microphone, you cannot turn on the microphone.
if roomInfo.isSeatEnabled, !currentUser.isOnSeat {
viewResponder?.makeToast(text: .muteSeatReasonText)
return
}
engineManager.unmuteLocalAudio()
guard !engineManager.store.audioSetting.isMicOpened else { return }
engineManager.openLocalMicrophone()
}
func checkMuteAudioHiddenState() -> Bool {
return roomInfo.isSeatEnabled && currentUser.userRole == .generalUser &&
!currentUser.isOnSeat
}
func checkMuteAudioSelectedState() -> Bool {
return !currentUser.hasAudioStream
}
}
extension LocalAudioViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_CurrentUserHasAudioStream:
viewResponder?.updateMuteAudioButton(isSelected: checkMuteAudioSelectedState())
case .TUIRoomKitService_CurrentUserRoleChanged, .TUIRoomKitService_UserOnSeatChanged:
if ableDisplay, !checkMuteAudioHiddenState() {
viewResponder?.show()
} else {
viewResponder?.hide()
}
default: break
}
}
}
private extension String {
static var muteAudioRoomReasonText: String {
localized("All on mute audio, unable to turn on microphone")
}
static var muteSeatReasonText: String {
localized("Can be turned on after taking the stage")
}
}

View File

@@ -0,0 +1,302 @@
//
// MediaSettingViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/16.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
protocol MediaSettingViewEventResponder: AnyObject {
func showResolutionAlert()
func showFrameRateAlert()
func showQualityView()
func updateStackView(item: ListCellItemData)
func makeToast(text: String)
}
class MediaSettingViewModel {
private(set) var videoItems: [ListCellItemData] = []
private(set) var audioItems: [ListCellItemData] = []
private(set) var otherItems: [ListCellItemData] = []
weak var viewResponder: MediaSettingViewEventResponder? = nil
var engineManager: EngineManager {
EngineManager.shared
}
var store: RoomStore {
engineManager.store
}
var videoSetting: VideoModel {
engineManager.store.videoSetting
}
var audioSetting: AudioModel {
engineManager.store.audioSetting
}
let resolutionNameItems: [String] = [.smoothResolutionText, .standardResolutionText, .highResolutionText, .superResolutionText]
private let resolutionItems: [TUIVideoQuality] = [.quality360P, .quality540P, .quality720P, .quality1080P]
private let bitrateArray = [550, 850, 1_200, 2_000]
let topItems: [String] = [.videoText, .audioText, .otherText]
let frameRateArray = ["15", "20"]
init() {
createVideoItem()
createAudioItem()
createOtherItem()
}
private func createVideoItem() {
let resolutionItem = ListCellItemData()
resolutionItem.titleText = .resolutionText
resolutionItem.hasOverAllAction = true
resolutionItem.type = .resolutionType
resolutionItem.hasDownLineView = true
resolutionItem.hasRightButton = true
let buttonData = ButtonItemData()
if let resolutionName = getResolutionName(videoQuality: videoSetting.videoQuality) {
buttonData.normalTitle = resolutionName
}
buttonData.orientation = .right
buttonData.normalIcon = "room_down_arrow1"
buttonData.resourceBundle = tuiRoomKitBundle()
buttonData.titleFont = UIFont.systemFont(ofSize: 16, weight: .regular)
buttonData.titleColor = UIColor(0xD1D9EC)
buttonData.size = CGSize(width: 80, height: 30)
buttonData.isEnabled = false
resolutionItem.buttonData = buttonData
resolutionItem.action = { [weak self] sender in
guard let self = self else { return }
self.resolutionAction()
}
videoItems.append(resolutionItem)
let frameRateItem = ListCellItemData()
frameRateItem.titleText = .frameRateText
frameRateItem.hasOverAllAction = true
frameRateItem.type = .frameRateType
frameRateItem.hasRightButton = true
let frameRateButtonData = ButtonItemData()
frameRateButtonData.orientation = .right
frameRateButtonData.normalIcon = "room_down_arrow1"
frameRateButtonData.normalTitle = String(videoSetting.videoFps)
frameRateButtonData.resourceBundle = tuiRoomKitBundle()
frameRateButtonData.titleFont = UIFont.systemFont(ofSize: 16, weight: .regular)
frameRateButtonData.titleColor = UIColor(0xD1D9EC)
frameRateButtonData.size = CGSize(width: 80, height: 30)
frameRateButtonData.isEnabled = false
frameRateItem.buttonData = frameRateButtonData
frameRateItem.action = { [weak self] sender in
guard let self = self else { return }
self.frameRateAction()
}
videoItems.append(frameRateItem)
}
private func createAudioItem() {
let captureVolumeItem = ListCellItemData()
captureVolumeItem.titleText = .captureVolumeText
captureVolumeItem.hasSlider = true
captureVolumeItem.hasSliderLabel = true
captureVolumeItem.minimumValue = 0
captureVolumeItem.maximumValue = 100
captureVolumeItem.sliderStep = 1
captureVolumeItem.sliderDefault = Float(audioSetting.captureVolume)
captureVolumeItem.hasDownLineView = true
captureVolumeItem.action = { [weak self] sender in
guard let self = self, let view = sender as? UISlider else { return }
self.captureVolumeAction(sender: view)
}
audioItems.append(captureVolumeItem)
let playingVolumeItem = ListCellItemData()
playingVolumeItem.titleText = .playVolumeText
playingVolumeItem.hasSlider = true
playingVolumeItem.hasSliderLabel = true
playingVolumeItem.minimumValue = 0
playingVolumeItem.maximumValue = 100
playingVolumeItem.sliderStep = 1
playingVolumeItem.sliderDefault = Float(audioSetting.playVolume)
playingVolumeItem.hasDownLineView = true
playingVolumeItem.action = { [weak self] sender in
guard let self = self, let view = sender as? UISlider else { return }
self.playingVolumeAction(sender: view)
}
audioItems.append(playingVolumeItem)
let volumePromptItem = ListCellItemData()
volumePromptItem.titleText = .volumePromptText
volumePromptItem.hasSwitch = true
volumePromptItem.isSwitchOn = audioSetting.volumePrompt
volumePromptItem.action = { [weak self] sender in
guard let self = self, let view = sender as? UISwitch else { return }
self.volumePromptAction(sender: view)
}
audioItems.append(volumePromptItem)
}
private func resolutionAction() {
viewResponder?.showResolutionAlert()
}
private func createOtherItem() {
let qualityItem = ListCellItemData()
qualityItem.titleText = .qualityInspectionText
qualityItem.hasOverAllAction = true
qualityItem.hasRightButton = true
let buttonData = ButtonItemData()
buttonData.orientation = .right
buttonData.normalIcon = "room_right_arrow1"
buttonData.resourceBundle = tuiRoomKitBundle()
buttonData.titleFont = UIFont.systemFont(ofSize: 16, weight: .regular)
buttonData.titleColor = UIColor(0xD1D9EC)
buttonData.size = CGSize(width: 80, height: 30)
buttonData.isEnabled = false
qualityItem.buttonData = buttonData
qualityItem.action = { [weak self] sender in
guard let self = self else { return }
self.showQualityAction()
}
otherItems.append(qualityItem)
let floatChatItem = ListCellItemData()
floatChatItem.titleText = .floatChatText
floatChatItem.hasSwitch = true
floatChatItem.isSwitchOn = store.shouldShowFloatChatView
floatChatItem.action = { [weak self] sender in
guard let self = self, let view = sender as? UISwitch else { return }
self.floatChatShowAction(shouldShow: view.isOn)
}
otherItems.append(floatChatItem)
}
private func showQualityAction() {
viewResponder?.showQualityView()
}
private func floatChatShowAction(shouldShow: Bool) {
store.updateFloatChatShowState(shouldShow: shouldShow)
}
private func frameRateAction() {
viewResponder?.showFrameRateAlert()
}
private func captureVolumeAction(sender: UISlider) {
engineManager.setAudioCaptureVolume(Int(sender.value))
}
private func playingVolumeAction(sender: UISlider) {
engineManager.setAudioPlayoutVolume(Int(sender.value))
}
private func volumePromptAction(sender: UISwitch) {
engineManager.enableAudioVolumeEvaluation(isVolumePrompt: sender.isOn)
}
func changeResolutionAction(index: Int) {
guard let videoItem = videoItems.first(where: { $0.type == .resolutionType }) else { return }
guard let quality = resolutionItems[safe: index] else { return }
guard let resolutionName = getResolutionName(videoQuality: quality) else { return }
videoItem.buttonData?.normalTitle = resolutionName
viewResponder?.updateStackView(item: videoItem)
engineManager.setVideoEncoder(videoQuality: quality, bitrate: getBitrate(videoQuality: quality))
}
func changeFrameRateAction(index: Int) {
guard let videoItem = videoItems.first(where: { $0.type == .frameRateType }) else { return }
guard let frameRate = frameRateArray[safe: index] else { return }
videoItem.buttonData?.normalTitle = frameRate
viewResponder?.updateStackView(item: videoItem)
engineManager.setVideoEncoder(fps: Int(frameRate))
}
func getCurrentResolutionIndex() -> Int {
guard let index = resolutionItems.firstIndex(where: { $0 == videoSetting.videoQuality }) else { return 0 }
return index
}
func getCurrentFrameRateIndex() -> Int {
let frameRateString = String(videoSetting.videoFps)
guard let index = frameRateArray.firstIndex(where: { $0 == frameRateString }) else { return 0 }
return index
}
private func getResolutionName(videoQuality: TUIVideoQuality) -> String? {
guard let index = resolutionItems.firstIndex(of: videoQuality) else { return nil }
guard let resolutionName = resolutionNameItems[safe: index] else { return nil }
return resolutionName
}
private func getBitrate(videoQuality: TUIVideoQuality) -> Int? {
guard let index = resolutionItems.firstIndex(of: videoQuality) else { return nil }
guard let bitrate = bitrateArray[safe: index] else { return nil }
return bitrate
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var videoText: String {
localized("Video Settings")
}
static var audioText: String {
localized("Audio Settings")
}
static var otherText: String {
localized("Other Settings")
}
static var versionLowToastText: String {
localized("Your system version is below 12.0. Please update.")
}
static var resolutionText: String {
localized("Resolution")
}
static var frameRateText: String {
localized("Frame Rate")
}
static var bitrateText: String {
localized("Bitrate")
}
static var localMirrorText: String {
localized("Local Mirror")
}
static var captureVolumeText: String {
localized("Capture Volume")
}
static var playVolumeText: String {
localized("Playback Volume")
}
static var volumePromptText: String {
localized("Volume Reminder")
}
static var audioRecordingText: String {
localized("Audio Recording")
}
static var smoothResolutionText: String {
localized("Smooth")
}
static var standardResolutionText: String {
localized("Standard Definition")
}
static var highResolutionText: String {
localized("High Definition")
}
static var superResolutionText: String {
localized("Full High Definition")
}
static var qualityInspectionText: String {
localized("Network Quality Monitoring")
}
static var floatChatText: String {
localized("Floating Chat")
}
}

View File

@@ -0,0 +1,116 @@
//
// MemberInviteViewModel.swift
// TUIRoomKit
//
// Created by krabyu on 2023/8/21.
//
import Foundation
import RTCRoomEngine
protocol MemberInviteResponder : NSObjectProtocol {
func showCopyToast(copyType: CopyType?)
}
class MemberInviteViewModel {
var title: String = .inviteMembersToJoin
private(set) lazy var messageItems: [ListCellItemData] = {
return generateListData()
}()
private lazy var roomInfo = {
EngineManager.shared.store.roomInfo
}()
private lazy var conferenceInfoDetails = {
title
}()
private var roomLink: String? {
guard let bundleId = Bundle.main.bundleIdentifier else { return nil }
if bundleId == "com.tencent.tuiroom.apiexample" || bundleId == "com.tencent.fx.rtmpdemo" {
return "https://web.sdk.qcloud.com/trtc/webrtc/test/tuiroom-inner/index.html#/" + "room?roomId=" + roomInfo.roomId
} else if bundleId == "com.tencent.mrtc" {
return "https://web.sdk.qcloud.com/component/tuiroom/index.html#/" + "room?roomId=" + roomInfo.roomId
} else {
return nil
}
}
weak var viewResponder: MemberInviteResponder?
func createListCellItemData(titleText: String, messageText: String,
hasButton: Bool, copyType: CopyType?) -> ListCellItemData {
let item = ListCellItemData()
item.titleText = titleText
item.messageText = messageText
item.hasRightButton = hasButton
if hasButton {
let buttonData = ButtonItemData()
buttonData.normalIcon = "room_copy"
buttonData.normalTitle = .copyText
buttonData.cornerRadius = 4
buttonData.titleFont = UIFont(name: "PingFangSC-Regular", size: 12)
buttonData.titleColor = UIColor(0xB2BBD1)
buttonData.backgroundColor = UIColor(0x6B758A).withAlphaComponent(0.7)
buttonData.resourceBundle = tuiRoomKitBundle()
buttonData.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.copyAction(sender: button, text: item.messageText,copyType: copyType)
}
conferenceInfoDetails = conferenceInfoDetails + "\n\(titleText) : \(messageText)"
item.buttonData = buttonData
}
return item
}
func generateListData() -> [ListCellItemData] {
var array: [ListCellItemData] = []
let roomNametem = createListCellItemData(titleText: .roomName, messageText: roomInfo.name, hasButton: false, copyType: nil)
array.append(roomNametem)
let roomTypeItem = createListCellItemData(titleText: .roomType, messageText: roomInfo.isSeatEnabled ? .onStageSpeechRoom : .freeSpeechRoom, hasButton: false, copyType: nil)
array.append(roomTypeItem)
let roomIdItem = createListCellItemData(titleText: .roomIdText, messageText: roomInfo.roomId, hasButton: true, copyType: .copyRoomIdType)
array.append(roomIdItem)
if roomInfo.password.count > 0 {
let roomPasswordItem = createListCellItemData(titleText: .roomPassword, messageText: roomInfo.password, hasButton: true, copyType: .copyRoomPassword)
array.append(roomPasswordItem)
}
if let roomLink = roomLink {
let roomLinkItem = createListCellItemData(titleText: .roomLinkText, messageText: roomLink, hasButton: true, copyType: .copyRoomLinkType)
array.append(roomLinkItem)
}
return array
}
func copyAction(sender: UIButton, text: String, copyType: CopyType?) {
UIPasteboard.general.string = text
viewResponder?.showCopyToast(copyType: copyType)
}
func copyAction() {
UIPasteboard.general.string = conferenceInfoDetails
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var roomIdText: String {
localized("ConferenceID")
}
static var roomLinkText: String {
localized("Link")
}
static var copyText: String {
localized("Copy")
}
static var inviteMemberText: String {
localized("Invite member")
}
static let roomPassword = localized("Conference password")
static let conferencePasswordSuccess = localized("Conference password copied successfully.")
static let roomName = localized("Room name")
static let roomType = localized("Room type")
static let freeSpeechRoom = localized("Free Speech Room")
static let onStageSpeechRoom = localized("On-stage Speech Room")
static let inviteMembersToJoin = localized("Invite Others")
}

View File

@@ -0,0 +1,77 @@
//
// MoreFunctionViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/12.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import TUICore
class MoreFunctionViewModel {
private(set) var viewItems: [ButtonItemData] = []
var engineManager: EngineManager {
return EngineManager.shared
}
var engineEventCenter: EngineEventCenter {
return EngineEventCenter.shared
}
init() {
createBottomData()
}
func createBottomData() {
if hasTUIChatItem() {
let chatItem = ButtonItemData()
chatItem.normalIcon = "room_chat"
chatItem.normalTitle = .chatText
chatItem.resourceBundle = tuiRoomKitBundle()
chatItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.chatAction(sender: button)
}
viewItems.append(chatItem)
}
let settingItem = ButtonItemData()
settingItem.normalIcon = "room_setting"
settingItem.normalTitle = .settingText
settingItem.resourceBundle = tuiRoomKitBundle()
settingItem.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.settingAction(sender: button)
}
viewItems.append(settingItem)
}
private func hasTUIChatItem() -> Bool {
return TUICore.getService(TUICore_TUIChatService) != nil
}
func settingAction(sender: UIButton) {
sender.isSelected = !sender.isSelected
RoomRouter.shared.presentPopUpViewController(viewType: .mediaSettingViewType, height: 300.scale375())
}
func chatAction(sender: UIButton) {
RoomRouter.shared.dismissPopupViewController()
let user = engineManager.store.currentUser
let roomInfo = engineManager.store.roomInfo
RoomRouter.shared.pushToChatController(user: user, roomInfo: roomInfo)
}
deinit {
debugPrint("deinit \(self)")
}
}
private extension String {
static var settingText: String {
localized("Settings")
}
static var chatText: String {
localized("Chat")
}
}

View File

@@ -0,0 +1,53 @@
//
// PopUpViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/12.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import TUICore
enum PopUpViewType {
case roomInfoViewType
case moreViewType
case mediaSettingViewType
case userListViewType
case raiseHandApplicationListViewType
case transferMasterViewType
case QRCodeViewType
case chatViewType
case inviteViewType
case inviteMemberViewType
}
protocol PopUpViewModelResponder: AnyObject {
func searchControllerChangeActive(isActive: Bool)
func updateViewOrientation(isLandscape: Bool)
}
class PopUpViewModel {
let viewType: PopUpViewType
let height: CGFloat
var backgroundColor: UIColor?
weak var viewResponder: PopUpViewModelResponder?
init(viewType: PopUpViewType, height: CGFloat) {
self.viewType = viewType
self.height = height
}
func panelControlAction() {
changeSearchControllerActive()
RoomRouter.shared.dismissPopupViewController()
}
func changeSearchControllerActive() {
viewResponder?.searchControllerChangeActive(isActive: false)
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,69 @@
//
// QRCodeViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/11.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import PhotosUI
class QRCodeViewModel {
let urlString: String
var store: RoomStore {
EngineManager.shared.store
}
init(urlString: String) {
self.urlString = urlString
}
func copyAction(sender: UIButton, text: String) {
UIPasteboard.general.string = text
}
func saveIntoAlbumAction(sender: UIButton, image: UIImage) {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
func backAction() {
RoomRouter.shared.dismissPopupViewController()
}
func createQRCodeImageView(url: String, imageView: UIImageView) {
if url.count == 0 { return }
guard let filter = CIFilter(name: "CIQRCodeGenerator") else {return}
filter.setDefaults()
// Set filter input data
let data = url.data(using: String.Encoding.utf8)
filter.setValue(data, forKey: "inputMessage")
// Set the error correction rate of QR code
filter.setValue("M", forKey: "inputCorrectionLevel")
guard var image = filter.outputImage else { return }
let transform = CGAffineTransform(scaleX: 20, y: 20)
image = image.transformed(by: transform)
let resultImage = UIImage(ciImage: image)
guard let center = UIImage(named: "AppIcon.png") else { return }
guard let resultImage = getClearImage(sourceImage: resultImage, center: center) else { return }
imageView.image = resultImage
}
private func getClearImage(sourceImage: UIImage, center: UIImage) -> UIImage? {
let size = sourceImage.size
UIGraphicsBeginImageContext(size)
sourceImage.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
let width: CGFloat = 80
let height: CGFloat = 80
let x: CGFloat = (size.width - width) * 0.5
let y: CGFloat = (size.height - height) * 0.5
center.draw(in: CGRect(x: x, y: y, width: width, height: height))
guard let resultImage = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return resultImage
}
deinit {
debugPrint("deinit \(self)")
}
}

View File

@@ -0,0 +1,190 @@
//
// QualityInfoViewModel.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/4/19.
// Copyright © 2024 Tencent. All rights reserved.
//
import Foundation
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
protocol QualityViewResponder: AnyObject {
func reloadData()
}
class QualityCellModel: NSObject {
enum CellType {
case upDown
case normal
}
var titleText: String = ""
var type: CellType = .upDown
var uplinkString: String = ""
var downlinkString: String = ""
var normalString: String = ""
}
class QualitySectionModel: NSObject {
var titleText: String = ""
var items: [QualityCellModel] = []
}
class QualityInfoViewModel: NSObject {
static let timeSuffix = "ms"
static let lossSuffix = "%"
static let bitrateSuffix = "kbps"
static let framerateSuffix = "FPS"
var sections: [QualitySectionModel] = []
weak var viewResponder: QualityViewResponder? = nil
private var rttCellModel: QualityCellModel = {
var rttCellModel = QualityCellModel()
rttCellModel.titleText = .rttString
rttCellModel.normalString = "0" + timeSuffix
rttCellModel.type = .normal
return rttCellModel
}()
private var lossCellModel: QualityCellModel = {
var lossCellModel = QualityCellModel()
lossCellModel.titleText = .lossString
lossCellModel.uplinkString = "0" + lossSuffix
lossCellModel.downlinkString = "0" + lossSuffix
return lossCellModel
}()
private var audioBitrateCellModel: QualityCellModel = {
var bitrateCellModel = QualityCellModel()
bitrateCellModel.titleText = .bitrateString
bitrateCellModel.uplinkString = "0" + bitrateSuffix
bitrateCellModel.downlinkString = "0" + bitrateSuffix
return bitrateCellModel
}()
private var videoResCellModel: QualityCellModel = {
var resCellModel = QualityCellModel()
resCellModel.titleText = .resolutionString
resCellModel.uplinkString = "0x0"
resCellModel.downlinkString = "0x0"
return resCellModel
}()
private var videoFrameRateCellModel: QualityCellModel = {
var frameCellModel = QualityCellModel()
frameCellModel.titleText = .frameRateString
frameCellModel.uplinkString = "0" + framerateSuffix
frameCellModel.downlinkString = "0" + framerateSuffix
return frameCellModel
}()
private var videoBitrateCellModel: QualityCellModel = {
var bitrateCellModel = QualityCellModel()
bitrateCellModel.titleText = .bitrateString
bitrateCellModel.uplinkString = "0" + bitrateSuffix
bitrateCellModel.downlinkString = "0" + bitrateSuffix
return bitrateCellModel
}()
override init() {
super.init()
self.addSection(with: [self.rttCellModel, self.lossCellModel], title: .networkString)
self.addSection(with: [self.audioBitrateCellModel], title: .audioString)
self.addSection(with: [self.videoResCellModel, self.videoFrameRateCellModel, self.videoBitrateCellModel], title: .videoString)
subscribeEngine()
}
private func addSection(with items: [QualityCellModel], title: String) {
let section = QualitySectionModel()
section.titleText = title
section.items = items
self.sections.append(section)
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onStatistics, observer: self)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onStatistics, observer: self)
}
deinit {
unsubscribeEngine()
}
}
extension QualityInfoViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onStatistics:
guard let data = param?["statistics"] as? TRTCStatistics else { return }
handleStatistics(data: data)
default:
break
}
}
private func handleStatistics(data: TRTCStatistics) {
let localStatistics = data.localStatistics.first(where: { $0.streamType == .big })
let remoteStatistics = data.remoteStatistics ?? []
self.rttCellModel.normalString = String(data.rtt) + QualityInfoViewModel.timeSuffix
// assemble uplink data
self.lossCellModel.uplinkString = String(data.upLoss) + QualityInfoViewModel.lossSuffix
self.audioBitrateCellModel.uplinkString = String(localStatistics?.audioBitrate ?? 0) + QualityInfoViewModel.bitrateSuffix
self.videoResCellModel.uplinkString = String(localStatistics?.width ?? 0) + "x" + String(localStatistics?.height ?? 0)
self.videoFrameRateCellModel.uplinkString = String(localStatistics?.frameRate ?? 0) + QualityInfoViewModel.framerateSuffix
self.videoBitrateCellModel.uplinkString = String(localStatistics?.videoBitrate ?? 0) + QualityInfoViewModel.bitrateSuffix
// assemble downlink data
let remoteSumAudioBitrate = remoteStatistics.reduce(0) { sum, stream in
return sum + stream.audioBitrate
}
let remoteMaxFramerate = remoteStatistics.max(by: {$0.frameRate < $1.frameRate})
let remoteMaxVideoRes = remoteStatistics.max(by: { $0.width * $0.height < $1.width * $1.height })
let remoteSumVideoBitrate = remoteStatistics.reduce(0) { sum, stream in
return sum + stream.videoBitrate
}
self.lossCellModel.downlinkString = String(data.downLoss) + QualityInfoViewModel.lossSuffix
self.audioBitrateCellModel.downlinkString = String(remoteSumAudioBitrate) + QualityInfoViewModel.bitrateSuffix
self.videoResCellModel.downlinkString = String(remoteMaxVideoRes?.width ?? 0) + "x" + String(remoteMaxVideoRes?.height ?? 0)
self.videoFrameRateCellModel.downlinkString = String(remoteMaxFramerate?.frameRate ?? 0) + QualityInfoViewModel.framerateSuffix
self.videoBitrateCellModel.downlinkString = String(remoteSumVideoBitrate) + QualityInfoViewModel.bitrateSuffix
self.viewResponder?.reloadData()
}
}
private extension String {
static var networkString: String {
localized("Network")
}
static var audioString: String {
localized("Audio")
}
static var videoString: String {
localized("Video")
}
static var rttString: String {
localized("Latency")
}
static var lossString: String {
localized("Packet Loss Rate")
}
static var bitrateString: String {
localized("Bitrate")
}
static var resolutionString: String {
localized("Resolution")
}
static var frameRateString: String {
localized("Frame Rate")
}
}

View File

@@ -0,0 +1,96 @@
//
// RaiseHandApplicationListViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/13.
// Copyright © 2023 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
protocol RaiseHandApplicationListViewResponder: NSObject {
func reloadApplyListView()
func makeToast(text: String)
func updatePlaceholderViewState(isShown: Bool)
func updateApplyButtonState(isEnabled: Bool)
}
class RaiseHandApplicationListViewModel: NSObject {
weak var viewResponder: RaiseHandApplicationListViewResponder? = nil
var engineManager: EngineManager {
EngineManager.shared
}
var roomInfo: TUIRoomInfo {
engineManager.store.roomInfo
}
var inviteSeatList: [RequestEntity] = []
var isPlaceholderViewShown: Bool {
return inviteSeatList.isEmpty
}
var isApplyButtonEnabled: Bool {
return !inviteSeatList.isEmpty
}
override init() {
super.init()
inviteSeatList = engineManager.store.inviteSeatList
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
}
func respondAllRequest(isAgree: Bool) {
var isShownStageFullToast = false
for requestEntity in engineManager.store.inviteSeatList {
engineManager.responseRemoteRequest(requestEntity.requestId, agree: isAgree) { [weak self] in
guard let self = self else { return }
self.engineManager.store.deleteTakeSeatRequest(requestId: requestEntity.requestId)
} onError: { [weak self] code, message in
guard let self = self else { return }
guard code == .allSeatOccupied, !isShownStageFullToast else { return }
self.viewResponder?.makeToast(text: .onStageNumberReachedLimitText)
isShownStageFullToast = true
}
}
}
func respondRequest(isAgree: Bool, request: RequestEntity) {
engineManager.responseRemoteRequest(request.requestId, agree: isAgree) { [weak self] in
guard let self = self else { return }
self.engineManager.store.deleteTakeSeatRequest(requestId: request.requestId)
self.reloadApplyListView()
} onError: { [weak self] code, message in
guard let self = self else { return }
guard code == .allSeatOccupied else { return }
self.viewResponder?.makeToast(text: .onStageNumberReachedLimitText)
}
}
func reloadApplyListView() {
inviteSeatList = engineManager.store.inviteSeatList
viewResponder?.updatePlaceholderViewState(isShown: isPlaceholderViewShown)
viewResponder?.updateApplyButtonState(isEnabled: isApplyButtonEnabled)
viewResponder?.reloadApplyListView()
}
deinit {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
debugPrint("deinit \(self)")
}
}
extension RaiseHandApplicationListViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
if key == .TUIRoomKitService_RenewSeatList {
self.reloadApplyListView()
}
}
}
private extension String {
static var onStageNumberReachedLimitText: String {
localized("The stage is full")
}
}

View File

@@ -0,0 +1,104 @@
//
// RaiseHandApplicationNotificationViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2024/5/16.
//
import Foundation
import RTCRoomEngine
protocol RaiseHandApplicationNotificationViewModelResponder: AnyObject {
func showRaiseHandApplicationNotificationView(userId: String, userName: String, count: Int)
func hideRaiseHandApplicationNotificationView()
}
class RaiseHandApplicationNotificationViewModel: NSObject {
var delayDisappearanceTime = 5.0
lazy var userId: String? = {
return inviteSeatList.last?.userId
}()
lazy var userName: String? = {
return inviteSeatList.last?.userName
}()
lazy var applicationCount: Int? = {
return inviteSeatList.count
}()
weak var responder: RaiseHandApplicationNotificationViewModelResponder?
var inviteSeatList: [RequestEntity] {
EngineManager.shared.store.inviteSeatList
}
lazy var isShownRaiseHandApplicationNotificationView: Bool = {
return getShownRequestEntity() != nil
}()
override init() {
super.init()
subscribeEngine()
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onRequestReceived, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onDeletedTakeSeatRequest, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onRequestReceived, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onDeletedTakeSeatRequest, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewSeatList, responder: self)
}
private func getShownRequestEntity() -> RequestEntity? {
let currentTime = Date().timeIntervalSince1970
guard let lastItem = inviteSeatList.last else { return nil }
if delayDisappearanceTime > 0, currentTime - lastItem.timestamp > delayDisappearanceTime {
return nil
} else {
return lastItem
}
}
func checkRaiseHandApplicationAction() {
RoomRouter.shared.presentPopUpViewController(viewType: .raiseHandApplicationListViewType, height: 720.scale375Height(), backgroundColor: UIColor(0x22262E))
}
deinit {
unsubscribeEngine()
}
}
extension RaiseHandApplicationNotificationViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onRequestReceived:
guard let request = param?["request"] as? TUIRequest else { return }
guard request.requestAction == .takeSeat else { return }
self.userId = request.userId
self.userName = request.userName
responder?.showRaiseHandApplicationNotificationView(userId: request.userId, userName: request.userName, count: inviteSeatList.count)
case .onDeletedTakeSeatRequest:
guard let userId = param?["userId"] as? String else { return }
guard userId == self.userId else { return }
let requestItem = getShownRequestEntity()
self.userId = requestItem?.userId
self.userName = requestItem?.userName
if let requestItem = requestItem {
responder?.showRaiseHandApplicationNotificationView(userId: requestItem.userId, userName: requestItem.userName, count: inviteSeatList.count)
} else {
responder?.hideRaiseHandApplicationNotificationView()
}
default: break
}
}
}
extension RaiseHandApplicationNotificationViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
if key == .TUIRoomKitService_RenewSeatList {
guard let requestItem = getShownRequestEntity() else { return }
responder?.showRaiseHandApplicationNotificationView(userId: requestItem.userId, userName: requestItem.userName, count: inviteSeatList.count)
}
}
}

View File

@@ -0,0 +1,163 @@
//
// RoomInfoViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/1/3.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
enum CopyType {
case copyRoomIdType
case copyRoomLinkType
case copyRoomPassword
}
protocol RoomInfoResponder : NSObjectProtocol {
func showCopyToast(copyType: CopyType?)
func updateNameLabel(_ text: String)
}
class RoomInfoViewModel: NSObject {
private(set) var messageItems: [ListCellItemData] = []
var store: RoomStore {
EngineManager.shared.store
}
var roomInfo: TUIRoomInfo {
store.roomInfo
}
lazy var title = {
roomInfo.name
}()
weak var viewResponder: RoomInfoResponder?
var roomLink: String? {
guard let bundleId = Bundle.main.bundleIdentifier else { return nil }
if bundleId == "com.tencent.tuiroom.apiexample" || bundleId == "com.tencent.fx.rtmpdemo" {
return "https://web.sdk.qcloud.com/trtc/webrtc/test/tuiroom-inner/index.html#/" + "room?roomId=" + roomInfo.roomId
} else if bundleId == "com.tencent.mrtc" {
return "https://web.sdk.qcloud.com/component/tuiroom/index.html#/" + "room?roomId=" + roomInfo.roomId
} else {
return nil
}
}
private lazy var conferenceDetails = {
title
}()
override init() {
super.init()
subscribeEngine()
createSourceData()
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onConferenceInfoChanged, observer: self)
}
private func unsubscribeUIEvent() {
EngineEventCenter.shared.unsubscribeEngine(event: .onConferenceInfoChanged, observer: self)
}
func createListCellItemData(titleText: String, messageText: String,
hasButton: Bool, copyType: CopyType?) -> ListCellItemData {
let item = ListCellItemData()
item.titleText = titleText
item.messageText = messageText
item.hasRightButton = hasButton
if item.hasRightButton {
let buttonData = ButtonItemData()
buttonData.normalIcon = "room_copy"
buttonData.normalTitle = .copyText
buttonData.cornerRadius = 4
buttonData.titleFont = UIFont(name: "PingFangSC-Regular", size: 12)
buttonData.titleColor = UIColor(0xB2BBD1)
buttonData.backgroundColor = UIColor(0x6B758A).withAlphaComponent(0.7)
buttonData.resourceBundle = tuiRoomKitBundle()
buttonData.action = { [weak self] sender in
guard let self = self, let button = sender as? UIButton else { return }
self.copyAction(sender: button, text: item.messageText,copyType: copyType)
}
item.buttonData = buttonData
}
conferenceDetails = conferenceDetails + "\n\(titleText) : \(messageText)"
return item
}
func createSourceData() {
var userName = roomInfo.ownerId
if let userModel = store.attendeeList.first(where: { $0.userId == roomInfo.ownerId}) {
userName = userModel.userName
}
let roomHostItem = createListCellItemData(titleText: .roomHostText, messageText: userName, hasButton: false, copyType: nil)
messageItems.append(roomHostItem)
let roomTypeItem = createListCellItemData(titleText: .roomTypeText, messageText: roomInfo.isSeatEnabled ? .raiseHandSpeakText: .freedomSpeakText, hasButton: false, copyType: nil)
messageItems.append(roomTypeItem)
let roomIdItem = createListCellItemData(titleText: .roomIdText, messageText: roomInfo.roomId, hasButton: true, copyType: .copyRoomIdType)
messageItems.append(roomIdItem)
if roomInfo.password.count > 0 {
let passwordItem = createListCellItemData(titleText: .conferencePasswordText, messageText: roomInfo.password, hasButton: true, copyType: .copyRoomPassword)
messageItems.append(passwordItem)
}
if let roomLink = roomLink {
let roomLinkItem = createListCellItemData(titleText: .roomLinkText, messageText: roomLink, hasButton: true, copyType: .copyRoomLinkType)
messageItems.append(roomLinkItem)
}
}
func copyAction(sender: UIButton, text: String, copyType: CopyType?){
UIPasteboard.general.string = text
viewResponder?.showCopyToast(copyType: copyType)
}
func codeAction(sender: UIButton) {
RoomRouter.shared.presentPopUpViewController(viewType: .QRCodeViewType, height: 720.scale375Height())
}
func copyConferenceDetails() {
UIPasteboard.general.string = conferenceDetails
}
deinit {
unsubscribeUIEvent()
debugPrint("deinit \(self)")
}
}
extension RoomInfoViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onConferenceInfoChanged:
guard let conferenceInfo = param?["conferenceInfo"] as? TUIConferenceInfo else { return }
guard let modifyFlag = param?["modifyFlag"] as? TUIConferenceModifyFlag else { return }
guard modifyFlag.contains(.roomName) else { return }
viewResponder?.updateNameLabel(conferenceInfo.basicRoomInfo.name)
default: break
}
}
}
private extension String {
static var freedomSpeakText: String {
localized("Free Speech Conference")
}
static var raiseHandSpeakText: String {
localized("On-stage Speaking Conference")
}
static var roomHostText: String {
localized("Host")
}
static var roomTypeText: String {
localized("Conference Type")
}
static var roomIdText: String {
localized("ConferenceID")
}
static var roomLinkText: String {
localized("Link")
}
static var copyText: String {
localized("Copy")
}
static let conferencePasswordText = localized("Conference password")
}

View File

@@ -0,0 +1,212 @@
//
// RoomVideoFloatViewModel.swift
// TUIRoomKit
//
// Created by janejntang on 2023/7/11.
//
import Foundation
import RTCRoomEngine
protocol RoomVideoFloatViewResponder: NSObject {
func updateUserStatus(user: UserEntity)
func updateUserAudioVolume(hasAudio: Bool, volume: Int)
func makeToast(text: String)
func showAvatarImageView(isShow: Bool)
}
class RoomVideoFloatViewModel: NSObject {
var userId: String = ""
var streamType: TUIVideoStreamType = .cameraStream
weak var renderView: UIView?
weak var viewResponder: RoomVideoFloatViewResponder?
var engineManager: EngineManager {
EngineManager.shared
}
var roomInfo: TUIRoomInfo {
engineManager.store.roomInfo
}
var currentUser: UserEntity {
engineManager.store.currentUser
}
override init() {
super.init()
subscribeEngine()
subLogoutNotification()
}
private func subscribeEngine() {
EngineEventCenter.shared.subscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRoomDismissed, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOutOfRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onKickedOffLine, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RoomOwnerChanged, responder: self)
}
private func unsubscribeEngine() {
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRoomDismissed, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOutOfRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onKickedOffLine, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RoomOwnerChanged, responder: self)
}
private func subLogoutNotification() {
NotificationCenter.default.addObserver(self,
selector: #selector(dismissFloatViewForLogout),
name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
private func unsubLogoutNotification() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.TUILogoutSuccess, object: nil)
}
func showRoomMainView() {
if engineManager.store.isEnteredRoom {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowRoomMainView, param: [:])
}
}
func showFloatWindowViewVideo(renderView: UIView?) {
self.renderView = renderView
if let userModel = getScreenUserModel() { //If someone is screen sharing, show the screen share first
showScreenStream(userModel: userModel)
} else { //Show host without screen sharing
showCameraStream()
}
}
func getUserEntity(userId: String) -> UserEntity? {
return engineManager.store.attendeeList.first(where: { $0.userId == userId })
}
@objc private func dismissFloatViewForLogout() {
RoomVideoFloatView.dismiss()
}
deinit {
unsubscribeEngine()
unsubLogoutNotification()
debugPrint("deinit \(self)")
}
}
extension RoomVideoFloatViewModel {
private func getScreenUserModel() -> UserEntity? {
return engineManager.store.attendeeList.first(where: { $0.hasScreenStream == true })
}
private func showScreenStream(userModel: UserEntity) {
let streamType: TUIVideoStreamType = userModel.userId == currentUser.userId ? .cameraStream : .screenStream
startPlayVideo(userId: userModel.userId, streamType: streamType)
changePlayingState(userId: userModel.userId, streamType: streamType)
viewResponder?.updateUserStatus(user: userModel)
viewResponder?.showAvatarImageView(isShow: false)
}
private func showCameraStream() {
guard let userModel = getUserEntity(userId: roomInfo.ownerId) ?? getUserEntity(userId: currentUser.userId) else { return }
changePlayingState(userId: userModel.userId, streamType: .cameraStream)
viewResponder?.updateUserStatus(user: userModel)
if userModel.hasVideoStream {
startPlayVideo(userId: userModel.userId, streamType: .cameraStream)
} else {
viewResponder?.showAvatarImageView(isShow: true)
}
}
private func startPlayVideo(userId: String, streamType: TUIVideoStreamType) {
if userId == currentUser.userId {
engineManager.setLocalVideoView(streamType: streamType, view: renderView)
} else {
engineManager.setRemoteVideoView(userId: userId, streamType: streamType, view: renderView)
engineManager.startPlayRemoteVideo(userId: userId, streamType: streamType)
}
viewResponder?.showAvatarImageView(isShow: false)
}
private func stopPlayVideo(userId: String, streamType: TUIVideoStreamType) {
if userId == currentUser.userId {
engineManager.setLocalVideoView(streamType: streamType, view: nil)
return
}
engineManager.setRemoteVideoView(userId: userId, streamType: streamType, view: nil)
guard let userItem = getUserEntity(userId: userId) else { return }
if streamType == .screenStream, userItem.hasScreenStream {
engineManager.stopPlayRemoteVideo(userId: userId, streamType: .screenStream)
} else if streamType == .cameraStream, userItem.hasVideoStream {
engineManager.stopPlayRemoteVideo(userId: userId, streamType: .cameraStream)
}
}
private func changePlayingState(userId: String, streamType: TUIVideoStreamType) {
self.userId = userId
self.streamType = streamType
}
}
extension RoomVideoFloatViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onKickedOutOfRoom, .onRoomDismissed:
engineManager.destroyEngineManager()
RoomVideoFloatView.dismiss()
case .onUserVideoStateChanged:
guard let userId = param?["userId"] as? String else { return }
guard let streamType = param?["streamType"] as? TUIVideoStreamType else { return }
guard let hasVideo = param?["hasVideo"] as? Bool else { return }
if streamType == .screenStream {
if hasVideo {
stopPlayVideo(userId: roomInfo.ownerId, streamType: .cameraStream)
guard let userModel = getUserEntity(userId: userId) else { return }
showScreenStream(userModel: userModel)
} else {
stopPlayVideo(userId: self.userId, streamType: .screenStream)
showCameraStream()
}
return
}
guard getScreenUserModel() == nil else { return } //If someone is screen sharing, don't show the host screen
guard userId == roomInfo.ownerId else { return }
if hasVideo {
startPlayVideo(userId: userId, streamType: streamType)
} else {
viewResponder?.showAvatarImageView(isShow: true)
}
case .onUserAudioStateChanged:
guard let userId = param?["userId"] as? String else { return }
guard let hasAudio = param?["hasAudio"] as? Bool else { return }
guard userId == self.userId else { return }
var volume = 0
if let userModel = getUserEntity(userId: self.userId) {
volume = userModel.userVoiceVolume
}
viewResponder?.updateUserAudioVolume(hasAudio: hasAudio, volume: volume)
case .onUserVoiceVolumeChanged:
guard let volumeNumber = param?[self.userId] as? NSNumber else { return }
guard let userModel = getUserEntity(userId: self.userId) else { return }
viewResponder?.updateUserAudioVolume(hasAudio: userModel.hasAudioStream, volume: volumeNumber.intValue)
case .onKickedOffLine:
RoomVideoFloatView.dismiss()
default: break
}
}
}
extension RoomVideoFloatViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_RoomOwnerChanged:
guard getScreenUserModel() == nil else { return } //If someone is screen sharing, don't show the host screen
stopPlayVideo(userId: self.userId, streamType: .cameraStream)
showCameraStream()
default: break
}
}
}

View File

@@ -0,0 +1,586 @@
//
// TUIVideoSeatPresenter.swift
// TUIVideoSeat
//
// Created by WesleyLei on 2022/9/28.
// Copyright © 2022 Tencent. All rights reserved.
//
import Foundation
import RTCRoomEngine
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
protocol TUIVideoSeatViewModelResponder: AnyObject {
func reloadData()
func insertItems(at indexPaths: [IndexPath])
func deleteItems(at indexPaths: [IndexPath])
func getVideoVisibleCell(_ item: VideoSeatItem) -> VideoSeatCell?
func getMoveMiniscreen() -> TUIVideoSeatDragCell
func updateMiniscreen(_ item: VideoSeatItem?)
func updateMiniscreenVolume(_ item: VideoSeatItem)
func updateVideoSeatCellUI(_ item: VideoSeatItem)
func updateSeatVolume(_ item: VideoSeatItem)
func showScreenCaptureMaskView(isShow: Bool)
func destroyVideoSeatResponder()
}
enum TUIVideoSeatViewType {
case unknown
case singleType
case pureAudioType
case largeSmallWindowType
case speechType
case equallyDividedType
}
class TUIVideoSeatViewModel: NSObject {
private var videoSeatItems: [VideoSeatItem] = []
private var shareItem: VideoSeatItem?
private var speakerItem: VideoSeatItem?
private var isSwitchPosition: Bool = false
private var speakerUpdateTimer: Int = 0
private let speakerUpdateTimeInterval = 5
private var itemStreamType: TUIVideoStreamType {
if listSeatItem.filter({ $0.hasVideoStream }).count > 5 {
return .cameraStreamLow
} else {
return .cameraStream
}
}
var listSeatItem: [VideoSeatItem] = []
private var isHasVideoStream: Bool {
return videoSeatItems.firstIndex(where: { $0.isHasVideoStream }) != nil
}
private var isHasScreenStream: Bool {
return shareItem != nil
}
weak var viewResponder: TUIVideoSeatViewModelResponder?
var videoSeatViewType: TUIVideoSeatViewType = .unknown
var engineManager: EngineManager {
EngineManager.shared
}
var store: RoomStore {
engineManager.store
}
var roomInfo: TUIRoomInfo {
store.roomInfo
}
var currentUserId: String {
store.currentUser.userId
}
override init() {
super.init()
initVideoSeatItems()
subscribeUIEvent()
}
private func initVideoSeatItems() {
videoSeatItems = []
let videoItems = store.roomInfo.isSeatEnabled ? store.seatList : store.attendeeList
guard videoItems.count > 0 else { return }
videoItems.forEach { userInfo in
let userItem = VideoSeatItem()
userItem.update(userInfo: userInfo)
videoSeatItems.append(userItem)
}
if let shareInfo = videoItems.first(where: { $0.hasScreenStream }) {
updateShareItem(userInfo: shareInfo)
}
sortSeatItems()
reloadSeatItems()
}
private func subscribeUIEvent() {
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, responder: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserScreenCaptureStopped, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onRemoteUserLeaveRoom, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onUserRoleChanged, observer: self)
EngineEventCenter.shared.subscribeEngine(event: .onSeatListChanged, observer: self)
EngineEventCenter.shared.subscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
}
private func unsubscribeUIEvent() {
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, responder: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserAudioStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVideoStateChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserVoiceVolumeChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserScreenCaptureStopped, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRemoteUserEnterRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onRemoteUserLeaveRoom, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onUserRoleChanged, observer: self)
EngineEventCenter.shared.unsubscribeEngine(event: .onSeatListChanged, observer: self)
EngineEventCenter.shared.unsubscribeUIEvent(key: .TUIRoomKitService_DismissConferenceViewController, responder: self)
}
func updateShareItem(userInfo: UserEntity) {
guard userInfo.hasScreenStream, shareItem == nil else { return }
let item = VideoSeatItem()
item.update(userInfo: userInfo)
item.videoStreamType = .screenStream
shareItem = item
}
deinit {
unsubscribeUIEvent()
debugPrint("deinit:\(self)")
}
}
extension TUIVideoSeatViewModel {
private func startPlayVideo(item: VideoSeatItem, renderView: UIView?) {
guard let renderView = renderView else { return }
if item.userId == currentUserId {
engineManager.setLocalVideoView(streamType: item.videoStreamType, view: renderView)
} else {
item.videoStreamType = item.videoStreamType == .screenStream ? .screenStream : itemStreamType
engineManager.setRemoteVideoView(userId: item.userId, streamType: item.videoStreamType, view: renderView)
engineManager.startPlayRemoteVideo(userId: item.userId, streamType: item.videoStreamType)
}
guard let seatCell = viewResponder?.getVideoVisibleCell(item) else { return }
seatCell.updateUI(item: item)
}
private func stopPlayVideo(item: VideoSeatItem) {
if item.userId == currentUserId {
engineManager.setLocalVideoView(streamType: item.videoStreamType, view: nil)
} else {
engineManager.setRemoteVideoView(userId: item.userId, streamType: item.videoStreamType, view: nil)
engineManager.stopPlayRemoteVideo(userId: item.userId, streamType: item.videoStreamType)
}
guard let seatCell = viewResponder?.getVideoVisibleCell(item) else { return }
seatCell.updateUI(item: item)
}
private func addUserInfo(_ userId: String) {
guard !videoSeatItems.contains(where: { $0.userId == userId }) else { return }
guard let userInfo = getUserInfo(userId: userId) else { return }
let seatItem = VideoSeatItem()
seatItem.update(userInfo: userInfo)
videoSeatItems.append(seatItem)
if checkNeededSort() {
refreshListSeatItem()
viewResponder?.reloadData()
resetMiniscreen()
} else {
reloadSeatItems()
}
}
private func getUserInfo(userId: String) -> UserEntity? {
return store.attendeeList.first(where: { $0.userId == userId })
}
private func removeSeatItem(_ userId: String) {
if shareItem?.userId == userId, let seatItem = shareItem {
stopPlayVideo(item: seatItem)
}
if speakerItem?.userId == userId, let seatItem = speakerItem {
stopPlayVideo(item: seatItem)
}
if let seatItem = videoSeatItems.first(where: { $0.userId == userId }) {
stopPlayVideo(item: seatItem)
}
videoSeatItems.removeAll(where: { $0.userId == userId })
var deleteIndex: [IndexPath] = []
if let index = listSeatItem.firstIndex(where: { $0.userId == userId && $0.videoStreamType != .screenStream }) {
deleteIndex.append(IndexPath(item: index, section: 0))
}
refreshListSeatItem()
if videoSeatViewType == .largeSmallWindowType {
viewResponder?.reloadData()
} else {
viewResponder?.deleteItems(at: deleteIndex)
}
resetMiniscreen()
}
private func changeUserRole(userId: String, userRole: TUIRole) {
if let item = getSeatItem(userId) {
item.userRole = userRole
viewResponder?.updateVideoSeatCellUI(item)
}
if let shareItem = shareItem, shareItem.userId == userId {
shareItem.userRole = userRole
viewResponder?.updateVideoSeatCellUI(shareItem)
}
if let speakerItem = speakerItem, speakerItem.userId == userId {
speakerItem.userRole = userRole
viewResponder?.updateVideoSeatCellUI(speakerItem)
}
guard userRole == .roomOwner else { return }
refreshListSeatItem()
viewResponder?.reloadData()
resetMiniscreen()
}
private func getSeatItem(_ userId: String) -> VideoSeatItem? {
return videoSeatItems.first(where: { $0.userId == userId })
}
private func sortSeatItems() {
guard checkNeededSort() else { return }
// I'm second
if let currentItemIndex = videoSeatItems.firstIndex(where: { $0.userId == self.currentUserId }) {
let currentItem = videoSeatItems.remove(at: currentItemIndex)
videoSeatItems.insert(currentItem, at: 0)
}
// Homeowners always come first
if let roomOwnerItemIndex = videoSeatItems.firstIndex(where: { $0.userId == roomInfo.ownerId }) {
let roomOwnerItem = videoSeatItems.remove(at: roomOwnerItemIndex)
videoSeatItems.insert(roomOwnerItem, at: 0)
}
}
private func checkNeededSort() -> Bool {
var isSort = false
if let roomOwnerItemIndex = videoSeatItems.firstIndex(where: { $0.userId == roomInfo.ownerId }) {
isSort = roomOwnerItemIndex != 0
}
if let currentItemIndex = videoSeatItems.firstIndex(where: { $0.userId == self.currentUserId }) {
if currentUserId == roomInfo.ownerId {
isSort = isSort || (currentItemIndex != 0)
} else {
isSort = isSort || (currentItemIndex != 1)
}
}
return isSort
}
private func findCurrentSpeaker(list: [VideoSeatItem]) -> VideoSeatItem? {
if let shareItem = shareItem {
return list.first(where: { $0.hasAudioStream && $0.userVoiceVolume > 10 && $0.userId != shareItem.userId })
} else if let speech = listSeatItem.first {
return list.first(where:{ $0.hasAudioStream && $0.userVoiceVolume > 10 && $0.userId != speech.userId })
}
return nil
}
private func refreshListSeatItem() {
sortSeatItems()
listSeatItem = Array(videoSeatItems)
if videoSeatItems.count == 1 {
videoSeatViewType = .singleType
if isHasScreenStream {
refreshMultiVideo()
}
} else if videoSeatItems.count == 2, isHasVideoStream, !isHasScreenStream {
videoSeatViewType = .largeSmallWindowType
if isSwitchPosition {
let first = listSeatItem[0]
listSeatItem[0] = listSeatItem[1]
listSeatItem[1] = first
}
} else if videoSeatItems.count >= 2, !isHasVideoStream, !isHasScreenStream {
videoSeatViewType = .pureAudioType
} else {
refreshMultiVideo()
}
}
private func refreshMultiVideo() {
let videoResult = videoSeatItems.filter({ $0.hasVideoStream })
var speechItem: VideoSeatItem?
if let item = shareItem {
speechItem = item
} else if videoResult.count == 1, let item = videoResult.first {
speechItem = item
}
if let item = speechItem, let seatItemIndex = videoSeatItems.firstIndex(where: { $0.userId == item.userId }) {
videoSeatViewType = .speechType
if item.videoStreamType == .screenStream, item.userId != currentUserId {
listSeatItem.insert(item, at: 0)
} else {
listSeatItem.remove(at: seatItemIndex)
listSeatItem.insert(item, at: 0)
if item.userId == speakerItem?.userId {
speakerItem = nil
}
}
if let currentSpeakerItem = findCurrentSpeaker(list: listSeatItem) {
speakerItem = currentSpeakerItem
} else {
if let item = speakerItem, videoSeatItems.firstIndex(where: { $0.userId == item.userId }) == nil {
speakerItem = nil
}
}
} else {
videoSeatViewType = .equallyDividedType
}
}
private func reloadSeatItems() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let type = self.videoSeatViewType
let lastListSeatItem = Array(self.listSeatItem)
self.refreshListSeatItem()
self.updateCollectionView(type, lastListSeatItem)
self.resetMiniscreen()
}
}
private func resetMiniscreen() {
if self.videoSeatViewType == .speechType {
self.viewResponder?.updateMiniscreen(self.speakerItem)
} else {
self.speakerItem = nil
self.viewResponder?.updateMiniscreen(nil)
}
}
private func updateSeatVolume(item: VideoSeatItem) {
viewResponder?.updateSeatVolume(item)
if let shareItem = shareItem, shareItem.userId == item.userId {
shareItem.hasAudioStream = item.hasAudioStream
shareItem.userVoiceVolume = item.userVoiceVolume
viewResponder?.updateSeatVolume(shareItem)
}
if let speakerItem = speakerItem, speakerItem.userId == item.userId {
speakerItem.hasAudioStream = item.hasAudioStream
speakerItem.userVoiceVolume = item.userVoiceVolume
viewResponder?.updateMiniscreenVolume(speakerItem)
}
}
private func updateCollectionView(_ type: TUIVideoSeatViewType, _ lastList: [VideoSeatItem]) {
if type != videoSeatViewType {
viewResponder?.reloadData()
} else {
let count = lastList.count
let diffItem = listSeatItem.count - count
var indexPaths: [IndexPath] = []
if diffItem > 0 {
for i in count ... (count + diffItem - 1) {
indexPaths.append(IndexPath(item: i, section: 0))
}
viewResponder?.insertItems(at: indexPaths)
}
for i in 0 ... min(max(count - 1, 0), max(listSeatItem.count - 1, 0)) {
guard lastList.count > i && listSeatItem.count > i && lastList[i] != listSeatItem[i] else { continue }
guard let item = listSeatItem[safe: i] else { continue }
viewResponder?.updateVideoSeatCellUI(item)
guard let cell = viewResponder?.getVideoVisibleCell(item) else { continue }
if item.hasVideoStream {
startPlayVideo(item: item, renderView: cell.renderView)
} else {
stopPlayVideo(item: item)
}
}
}
}
private func isConformedSpeakerTimeInterval() -> Bool {
let currentTime: TimeInterval = Date().timeIntervalSince1970
let timeStamp = Int(currentTime)
let totalTime: UInt = UInt(labs(timeStamp - speakerUpdateTimer))
return totalTime > speakerUpdateTimeInterval
}
}
extension TUIVideoSeatViewModel: RoomKitUIEventResponder {
func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) {
switch key {
case .TUIRoomKitService_RenewVideoSeatView:
initVideoSeatItems()
case .TUIRoomKitService_DismissConferenceViewController:
viewResponder?.destroyVideoSeatResponder()
viewResponder = nil
default: break
}
}
}
extension TUIVideoSeatViewModel: RoomEngineEventResponder {
func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) {
switch name {
case .onUserAudioStateChanged:
guard let userId = param?["userId"] as? String else { return }
guard let hasAudio = param?["hasAudio"] as? Bool else { return }
guard let seatItem = getSeatItem(userId) else { return }
seatItem.hasAudioStream = hasAudio
updateSeatVolume(item: seatItem)
case .onUserVoiceVolumeChanged:
guard let volumeMap = param as? [String: NSNumber] else { return }
userVoiceVolumeChanged(volumeMap: volumeMap)
case .onUserVideoStateChanged:
guard let userId = param?["userId"] as? String else { return }
guard let streamType = param?["streamType"] as? TUIVideoStreamType else { return }
guard let hasVideo = param?["hasVideo"] as? Bool else { return }
userVideoStateChanged(userId: userId, streamType: streamType, hasVideo: hasVideo)
case .onUserScreenCaptureStopped:
userScreenCaptureStopped()
case .onRemoteUserEnterRoom:
guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
guard !roomInfo.isSeatEnabled else { return }
addUserInfo(userInfo.userId)
case .onRemoteUserLeaveRoom:
guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return }
removeSeatItem(userInfo.userId)
case .onUserRoleChanged:
guard let userId = param?["userId"] as? String else { return }
guard let userRole = param?["userRole"] as? TUIRole else { return }
engineManager.fetchRoomInfo(roomId: roomInfo.roomId) { [weak self] _ in
guard let self = self else { return }
self.changeUserRole(userId: userId, userRole: userRole)
}
case .onSeatListChanged:
guard let left = param?["left"] as? [TUISeatInfo] else { return }
guard let seated = param?["seated"] as? [TUISeatInfo] else { return }
seatListChanged(seated: seated, left: left)
default: break
}
}
}
extension TUIVideoSeatViewModel: TUIRoomObserver {
private func userVoiceVolumeChanged(volumeMap: [String: NSNumber]) {
if volumeMap.count <= 0 {
return
}
for (userId, volume) in volumeMap {
guard let seatItem = getSeatItem(userId) else { continue }
seatItem.userVoiceVolume = volume.intValue
updateSeatVolume(item: seatItem)
}
guard videoSeatViewType == .speechType else { return }
guard let currentSpeakerItem = findCurrentSpeaker(list: listSeatItem) else { return }
if viewResponder?.getMoveMiniscreen().seatItem != nil, speakerItem?.userId == currentSpeakerItem.userId {
viewResponder?.updateMiniscreenVolume(currentSpeakerItem)
} else {
guard isConformedSpeakerTimeInterval() else { return }
viewResponder?.updateMiniscreen(currentSpeakerItem)
speakerUpdateTimer = Int(Date().timeIntervalSince1970)
}
speakerItem = currentSpeakerItem
}
private func userVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool) {
if streamType == .screenStream, userId == currentUserId {
viewResponder?.showScreenCaptureMaskView(isShow: hasVideo)
return
}
guard let seatItem = getSeatItem(userId) else { return }
if streamType == .cameraStream || streamType == .cameraStreamLow {
seatItem.hasVideoStream = hasVideo
if hasVideo {
setRemoteRenderParams(userId: userId, streamType: streamType)
startPlayVideo(item: seatItem, renderView: viewResponder?.getVideoVisibleCell(seatItem)?.renderView)
} else {
stopPlayVideo(item: seatItem)
}
reloadSeatItems()
} else {
updateScreenStreamView(seatItem: seatItem, hasVideo: hasVideo)
}
}
private func updateScreenStreamView(seatItem: VideoSeatItem, hasVideo: Bool) {
let screenIndexPath = IndexPath(item: 0, section: 0)
if hasVideo {
guard let shareUserInfo = getUserInfo(userId: seatItem.userId) else { return }
updateShareItem(userInfo: shareUserInfo)
refreshListSeatItem()
viewResponder?.insertItems(at: [screenIndexPath])
} else {
shareItem = nil
refreshListSeatItem()
if videoSeatViewType == .largeSmallWindowType {
viewResponder?.reloadData()
} else {
viewResponder?.deleteItems(at: [screenIndexPath])
}
}
speakerItem = nil
viewResponder?.updateMiniscreen(nil)
}
private func setRemoteRenderParams(userId: String, streamType: TUIVideoStreamType) {
let renderParams = TRTCRenderParams()
renderParams.fillMode = (streamType == .screenStream) ? .fit : .fill
let trtcStreamType: TRTCVideoStreamType = (streamType == .screenStream) ? .sub : .big
engineManager.setRemoteRenderParams(userId: userId, streamType: trtcStreamType, params: renderParams)
}
private func seatListChanged(seated: [TUISeatInfo], left: [TUISeatInfo]) {
for leftSeat in left {
if let userId = leftSeat.userId {
removeSeatItem(userId)
}
}
for seatInfo in seated {
if let userId = seatInfo.userId {
addUserInfo(userId)
}
}
}
private func userScreenCaptureStopped() {
viewResponder?.showScreenCaptureMaskView(isShow: false)
if shareItem?.userId == currentUserId {
shareItem = nil
}
reloadSeatItems()
}
}
extension TUIVideoSeatViewModel: TUIVideoSeatViewResponder {
func switchPosition() {
guard videoSeatViewType == .largeSmallWindowType else { return }
isSwitchPosition = !isSwitchPosition
refreshListSeatItem()
viewResponder?.reloadData()
resetMiniscreen()
}
func clickVideoSeat() {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ChangeToolBarHiddenState, param: [:])
guard RoomRouter.shared.hasChatWindow() else { return }
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_HiddenChatWindow, param: [:])
}
func startPlayVideoStream(item: VideoSeatItem, renderView: UIView?) {
startPlayVideo(item: item, renderView: renderView)
}
func stopPlayVideoStream(item: VideoSeatItem) {
stopPlayVideo(item: item)
}
func updateSpeakerPlayVideoState(currentPageIndex: Int) {
guard videoSeatViewType != .speechType else { return }
if currentPageIndex == 0 {
viewResponder?.updateMiniscreen(speakerItem)
} else if let item = videoSeatItems.first(where: { $0.userId == speakerItem?.userId }),
let renderView = viewResponder?.getVideoVisibleCell(item)?.renderView {
startPlayVideo(item: item, renderView: renderView)
}
}
func stopScreenCapture() {
EngineEventCenter.shared.notifyEngineEvent(event: .onUserScreenCaptureStopped, param: [:])
engineManager.stopScreenCapture()
}
}

Some files were not shown because too many files have changed in this diff Show More