提交
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
123
TUIKit/TUIRoomKit/Source/View/Component/ButtonItemView.swift
Normal file
123
TUIKit/TUIRoomKit/Source/View/Component/ButtonItemView.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
212
TUIKit/TUIRoomKit/Source/View/Component/ListCellItemView.swift
Normal file
212
TUIKit/TUIRoomKit/Source/View/Component/ListCellItemView.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
249
TUIKit/TUIRoomKit/Source/View/Component/QRCodeView.swift
Normal file
249
TUIKit/TUIRoomKit/Source/View/Component/QRCodeView.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
20
TUIKit/TUIRoomKit/Source/View/Model/AudioModel.swift
Normal file
20
TUIKit/TUIRoomKit/Source/View/Model/AudioModel.swift
Normal 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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
24
TUIKit/TUIRoomKit/Source/View/Model/ConferenceParams.swift
Normal file
24
TUIKit/TUIRoomKit/Source/View/Model/ConferenceParams.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
127
TUIKit/TUIRoomKit/Source/View/Model/ConferenceSessionImp.swift
Normal file
127
TUIKit/TUIRoomKit/Source/View/Model/ConferenceSessionImp.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
183
TUIKit/TUIRoomKit/Source/View/Model/EngineEventCenter.swift
Normal file
183
TUIKit/TUIRoomKit/Source/View/Model/EngineEventCenter.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
818
TUIKit/TUIRoomKit/Source/View/Model/EngineManager.swift
Normal file
818
TUIKit/TUIRoomKit/Source/View/Model/EngineManager.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
29
TUIKit/TUIRoomKit/Source/View/Model/RequestEntity.swift
Normal file
29
TUIKit/TUIRoomKit/Source/View/Model/RequestEntity.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
282
TUIKit/TUIRoomKit/Source/View/Model/RoomEventDispatcher.swift
Normal file
282
TUIKit/TUIRoomKit/Source/View/Model/RoomEventDispatcher.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
268
TUIKit/TUIRoomKit/Source/View/Model/RoomStore.swift
Normal file
268
TUIKit/TUIRoomKit/Source/View/Model/RoomStore.swift
Normal 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 })
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
27
TUIKit/TUIRoomKit/Source/View/Model/TRTCObserver.swift
Normal file
27
TUIKit/TUIRoomKit/Source/View/Model/TRTCObserver.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
45
TUIKit/TUIRoomKit/Source/View/Model/UserEntity.swift
Normal file
45
TUIKit/TUIRoomKit/Source/View/Model/UserEntity.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
19
TUIKit/TUIRoomKit/Source/View/Model/VideoModel.swift
Normal file
19
TUIKit/TUIRoomKit/Source/View/Model/VideoModel.swift
Normal 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
|
||||
}
|
||||
38
TUIKit/TUIRoomKit/Source/View/Model/VideoSeatItem.swift
Normal file
38
TUIKit/TUIRoomKit/Source/View/Model/VideoSeatItem.swift
Normal 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
|
||||
}
|
||||
}
|
||||
321
TUIKit/TUIRoomKit/Source/View/Page/ConferenceMainView.swift
Normal file
321
TUIKit/TUIRoomKit/Source/View/Page/ConferenceMainView.swift
Normal 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")
|
||||
}
|
||||
442
TUIKit/TUIRoomKit/Source/View/Page/RoomRouter.swift
Normal file
442
TUIKit/TUIRoomKit/Source/View/Page/RoomRouter.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// ConferenceMainViewState.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/9/3.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ConferenceMainViewState: Codable {
|
||||
var isInternalCreation = false
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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¤tPageItemCount&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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// WaterMarkLineStyle.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/4/7.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum WaterMarkLineStyle {
|
||||
case singleLine
|
||||
case multiLine
|
||||
}
|
||||
806
TUIKit/TUIRoomKit/Source/View/ViewModel/BottomViewModel.swift
Normal file
806
TUIKit/TUIRoomKit/Source/View/ViewModel/BottomViewModel.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
53
TUIKit/TUIRoomKit/Source/View/ViewModel/PopUpViewModel.swift
Normal file
53
TUIKit/TUIRoomKit/Source/View/ViewModel/PopUpViewModel.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
163
TUIKit/TUIRoomKit/Source/View/ViewModel/RoomInfoViewModel.swift
Normal file
163
TUIKit/TUIRoomKit/Source/View/ViewModel/RoomInfoViewModel.swift
Normal 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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user