From b3cc72ae16b2e9851591dd8446069c5f2ea48be9 Mon Sep 17 00:00:00 2001 From: Tangjianing <13102216755@163.com> Date: Fri, 15 Dec 2023 22:39:21 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90iOS=E3=80=91update=20version=20to=201.?= =?UTF-8?q?7.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ReleaseNotes.md | 4 + iOS/Example/App/AppDelegate.swift | 4 +- .../room_tencent_text.imageset/Contents.json | 23 - .../tencentText.png | Bin 5437 -> 0 bytes .../tencentText@2x.png | Bin 13200 -> 0 bytes .../tencentText@3x.png | Bin 24625 -> 0 bytes .../View/Component/ListCellItemView.swift | 1 - .../Component/RoomNavigationController.swift | 30 ++ .../Controller/CreateRoomViewController.swift | 66 ++- .../Controller/EnterRoomViewController.swift | 9 - .../RoomPrePareViewController.swift | 7 - .../App/Main/View/View/PrePareView.swift | 73 ++- .../localized/en.lproj/DemoLocalized.strings | 3 +- .../zh-Hans.lproj/DemoLocalized.strings | 3 +- iOS/Example/App/SceneDelegate.swift | 2 +- iOS/Example/DemoApp.xcodeproj/project.pbxproj | 6 +- .../Login/TRTCLoginViewController.swift | 8 + .../Login/TRTCRegisterViewController.swift | 8 + iOS/Example/Podfile | 18 +- .../ar.lproj/TUIRoomKitLocalized.strings | 7 +- .../en.lproj/TUIRoomKitLocalized.strings | 16 +- .../zh-Hans.lproj/TUIRoomKitLocalized.strings | 10 +- .../Model/Manager/RoomManager.swift | 1 + .../Source/Common/CGFloat+Extension.swift | 4 +- .../Source/Common/UIImage+RTL.swift | 2 +- .../Source/Model/EngineEventCenter.swift | 5 + .../Source/Model/EngineManager.swift | 56 +- .../Source/Model/RoomEventDispatcher.swift | 22 + iOS/TUIRoomKit/Source/Model/RoomStore.swift | 8 +- .../Source/Model/VideoSeatItem.swift | 52 +- .../View/Component/ListCellItemView.swift | 12 +- .../Source/View/Page/RoomMainView.swift | 32 +- .../Source/View/Page/RoomRouter.swift | 18 +- .../BottomNavigationBar/BottomView.swift | 25 +- .../Page/Widget/Dialog/ExitRoomView.swift | 25 +- .../RaiseHandApplicationListView.swift | 2 + .../TransferMasterView.swift | 4 + .../UserListManagerView.swift | 15 +- .../UserControlPanel/UserListView.swift | 54 +- .../VideoSeat/ScreenCaptureMaskView.swift | 18 +- ...ideoSeatCell.swift => VideoSeatCell.swift} | 105 ++-- ...SeatLayout.swift => VideoSeatLayout.swift} | 10 +- ...ew.swift => VideoSeatUserStatusView.swift} | 8 +- ...ideoSeatView.swift => VideoSeatView.swift} | 256 ++++----- .../Source/ViewModel/BottomViewModel.swift | 29 +- .../Source/ViewModel/ExitRoomViewModel.swift | 29 +- .../Source/ViewModel/RoomMainViewModel.swift | 29 +- .../ViewModel/TUIVideoSeatViewModel.swift | 485 +++++++++--------- .../ViewModel/TransferMasterViewModel.swift | 17 +- .../ViewModel/UserListManagerViewModel.swift | 28 +- .../Source/ViewModel/UserListViewModel.swift | 1 + iOS/TUIRoomKit/TUIRoomKit.podspec | 6 +- 52 files changed, 859 insertions(+), 797 deletions(-) delete mode 100644 iOS/Example/App/Assets.xcassets/room_tencent_text.imageset/Contents.json delete mode 100644 iOS/Example/App/Assets.xcassets/room_tencent_text.imageset/tencentText.png delete mode 100644 iOS/Example/App/Assets.xcassets/room_tencent_text.imageset/tencentText@2x.png delete mode 100644 iOS/Example/App/Assets.xcassets/room_tencent_text.imageset/tencentText@3x.png create mode 100644 iOS/Example/App/Main/View/Component/RoomNavigationController.swift rename iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/{TUIVideoSeatCell.swift => VideoSeatCell.swift} (77%) rename iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/{TUIVideoSeatLayout.swift => VideoSeatLayout.swift} (98%) rename iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/{TUIVideoSeatUserStatusView.swift => VideoSeatUserStatusView.swift} (94%) rename iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/{TUIVideoSeatView.swift => VideoSeatView.swift} (61%) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 4c83a0908..8029fa30b 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,9 @@ ## 发布日志 +### Version 1.7.0 @ 2023.12.15 +- iOS & Android: 优化界面,修改UI问题; +- iOS & Android: 修改roomId生成逻辑; + ### Version 1.6.1 @ 2023.11.10 - Android & iOS:优化产品体验,进一步美化横屏UI; - Android & iOS:优化产品体验,解决演讲者模式小画面切换抖动,设置小画面切换间隔为5秒; diff --git a/iOS/Example/App/AppDelegate.swift b/iOS/Example/App/AppDelegate.swift index 3b08e6177..f90691e20 100644 --- a/iOS/Example/App/AppDelegate.swift +++ b/iOS/Example/App/AppDelegate.swift @@ -45,14 +45,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return } let prePareViewController = RoomPrePareViewController() - let nav = UINavigationController(rootViewController: prePareViewController) + let nav = RoomNavigationController(rootViewController: prePareViewController) nav.modalPresentationStyle = .fullScreen getCurrentWindowViewController()?.present(nav, animated: true) } func showLoginViewController() { let loginVC = TRTCLoginViewController() - let nav = UINavigationController(rootViewController: loginVC) + let nav = RoomNavigationController(rootViewController: loginVC) if let keyWindow = SceneDelegate.getCurrentWindow() { keyWindow.rootViewController = nav keyWindow.makeKeyAndVisible() diff --git a/iOS/Example/App/Assets.xcassets/room_tencent_text.imageset/Contents.json b/iOS/Example/App/Assets.xcassets/room_tencent_text.imageset/Contents.json deleted file mode 100644 index 5fa55112c..000000000 --- a/iOS/Example/App/Assets.xcassets/room_tencent_text.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "tencentText.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "tencentText@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "tencentText@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iOS/Example/App/Assets.xcassets/room_tencent_text.imageset/tencentText.png b/iOS/Example/App/Assets.xcassets/room_tencent_text.imageset/tencentText.png deleted file mode 100644 index 4101166ee3a808c9c6f2dce391cbdf4b7e459e0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5437 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 String { + var numberOfDigit = numberOfDigits > 0 ? numberOfDigits : 1 + numberOfDigit = numberOfDigit < 10 ? numberOfDigit : 9 + let minNumber = Int(truncating: NSDecimalNumber(decimal: pow(10, numberOfDigit - 1))) + let maxNumber = Int(truncating: NSDecimalNumber(decimal: pow(10, numberOfDigit))) - 1 + let randomNumber = arc4random_uniform(UInt32(maxNumber - minNumber)) + UInt32(minNumber) + return String(randomNumber) + } + + private func checkIfRoomIdExists(roomId: String, onExist: @escaping () -> (), onNotExist: @escaping () -> ()) { + V2TIMManager.sharedInstance().getGroupsInfo([roomId]) { infoResult in + if infoResult?.first?.resultCode == 0 { + onExist() + } else { + onNotExist() + } + } fail: { code, message in + onNotExist() + } + } } private extension String { @@ -222,9 +244,6 @@ private extension String { static var roomTypeText: String { RoomDemoLocalize("Demo.TUIRoomKit.room.type") } - static var roomNumText: String { - RoomDemoLocalize("Demo.TUIRoomKit.room.num") - } static var openCameraText: String { RoomDemoLocalize("Demo.TUIRoomKit.open.video") } @@ -243,6 +262,9 @@ private extension String { static var videoConferenceText: String { RoomDemoLocalize("Demo.TUIRoomKit.video.conference") } + static var generatingRoomIdText: String { + RoomDemoLocalize("Demo.TUIRoomKit.generating.roomId") + } func truncateUtf8String(maxByteLength: Int) -> String { let length = self.utf8.count if length <= maxByteLength { diff --git a/iOS/Example/App/Main/View/Controller/EnterRoomViewController.swift b/iOS/Example/App/Main/View/Controller/EnterRoomViewController.swift index 88205c864..913d47537 100644 --- a/iOS/Example/App/Main/View/Controller/EnterRoomViewController.swift +++ b/iOS/Example/App/Main/View/Controller/EnterRoomViewController.swift @@ -62,16 +62,7 @@ class EnterRoomViewController: UIViewController { navigationController?.setNavigationBarHidden(false, animated: false) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton) UIApplication.shared.isIdleTimerDisabled = false - UIDevice.current.setValue(UIDeviceOrientation.portrait.rawValue, forKey: "orientation") renewRootViewState() - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } - appDelegate.orientation = .portrait - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } - appDelegate.orientation = .allButUpsideDown } @objc func backButtonClick(sender: UIButton) { diff --git a/iOS/Example/App/Main/View/Controller/RoomPrePareViewController.swift b/iOS/Example/App/Main/View/Controller/RoomPrePareViewController.swift index 3fdac31c9..843306c7f 100644 --- a/iOS/Example/App/Main/View/Controller/RoomPrePareViewController.swift +++ b/iOS/Example/App/Main/View/Controller/RoomPrePareViewController.swift @@ -31,13 +31,6 @@ class RoomPrePareViewController: UIViewController { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: false) UIApplication.shared.isIdleTimerDisabled = false - UIDevice.current.setValue(UIDeviceOrientation.portrait.rawValue, forKey: "orientation") - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } - appDelegate.orientation = .portrait } override func loadView() { diff --git a/iOS/Example/App/Main/View/View/PrePareView.swift b/iOS/Example/App/Main/View/View/PrePareView.swift index f0a36d369..b107c4dc2 100644 --- a/iOS/Example/App/Main/View/View/PrePareView.swift +++ b/iOS/Example/App/Main/View/View/PrePareView.swift @@ -7,6 +7,7 @@ import UIKit import TUICore +import TUIRoomEngine class PrePareView: UIView { @@ -49,25 +50,18 @@ class PrePareView: UIView { return button }() - let tencentBigView: UIView = { - let view = UIView(frame: .zero) - let iconImageView = UIImageView(frame: .zero) - iconImageView.image = UIImage(named: "room_tencent") - view.addSubview(iconImageView) - iconImageView.snp.makeConstraints { make in - make.top.equalToSuperview() - make.centerX.equalToSuperview() - make.width.equalTo(136) - make.height.equalTo(36) - } - let textImageView = UIImageView(frame: .zero) - textImageView.image = UIImage(named: "room_tencent_text") - view.addSubview(textImageView) - textImageView.snp.makeConstraints { make in - make.top.equalTo(iconImageView.snp.bottom).offset(5) - make.centerX.equalToSuperview() - } - return view + let signImageView: UIView = { + let imageView = UIImageView(frame: .zero) + imageView.image = UIImage(named: "room_tencent") + return imageView + }() + + let signLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 24, weight: .bold) + label.text = .videoConferencingText + return label }() let joinRoomButton: UIButton = { @@ -101,7 +95,9 @@ class PrePareView: UIView { return tip }() - init() { + private let signViewHeight: CGFloat = 36 + + init() { super.init(frame: .zero) } @@ -142,7 +138,8 @@ class PrePareView: UIView { topViewContainer.addSubview(userNameLabel) topViewContainer.addSubview(debugButton) topViewContainer.addSubview(switchLanguageButton) - addSubview(tencentBigView) + addSubview(signImageView) + addSubview(signLabel) addSubview(joinRoomButton) addSubview(createRoomButton) addSubview(appVersionTipLabel) @@ -176,13 +173,17 @@ class PrePareView: UIView { make.trailing.equalToSuperview().offset(-20) make.width.height.equalTo(30) } - tencentBigView.snp.makeConstraints { make in - make.width.equalTo(136.scale375()) - make.height.equalTo(36.scale375()) - make.leading.equalToSuperview().offset(119.scale375()) + signImageView.snp.makeConstraints { make in + make.width.equalTo(136) + make.height.equalTo(signViewHeight) + make.centerX.equalToSuperview() make.top.equalToSuperview().offset(157.scale375()) } - + signLabel.snp.makeConstraints { make in + make.top.equalTo(signImageView.snp.bottom).offset(5) + make.centerX.equalToSuperview() + make.height.equalTo(signViewHeight) + } joinRoomButton.snp.makeConstraints { make in make.height.equalTo(60.scale375()) make.width.equalTo(204.scale375()) @@ -217,6 +218,8 @@ class PrePareView: UIView { let placeholderImage = UIImage(named: "room_default_avatar") avatarButton.sd_setImage(with: URL(string: TUILogin.getFaceUrl() ?? ""), for: .normal, placeholderImage: placeholderImage) userNameLabel.text = TUILogin.getNickName() ?? "" + guard let image = getGradientImage(size: CGSize(width: UIScreen.main.bounds.width, height: signViewHeight)) else { return } + signLabel.textColor = UIColor(patternImage: image) } @objc @@ -244,6 +247,21 @@ class PrePareView: UIView { rootViewController?.switchLanguageAction() joinRoomButton.setTitle(.joinRoomText, for: .normal) createRoomButton.setTitle(.createRoomText, for: .normal) + signLabel.text = .videoConferencingText + } + + private func getGradientImage(size:CGSize) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale) + guard let context = UIGraphicsGetCurrentContext() else{ return nil } + let colorSpace = CGColorSpaceCreateDeviceRGB() + guard let gradientRef = CGGradient(colorsSpace: colorSpace, colors: [UIColor(0x00CED9).cgColor, UIColor(0x0C59F2).cgColor] + as CFArray, locations: nil) else { return nil } + let startPoint = CGPoint(x: 0, y: 0) + let endPoint = CGPoint(x: size.width, y: 0) + context.drawLinearGradient(gradientRef, start: startPoint, end: endPoint, options: CGGradientDrawingOptions(arrayLiteral: .drawsBeforeStartLocation,.drawsAfterEndLocation)) + let gradientImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return gradientImage } deinit { @@ -258,5 +276,8 @@ private extension String { static var createRoomText: String { RoomDemoLocalize("Demo.TUIRoomKit.create.room") } + static var videoConferencingText: String { + RoomDemoLocalize("Demo.TUIRoomKit.video.conferencing") + } } diff --git a/iOS/Example/App/Main/resource/localized/en.lproj/DemoLocalized.strings b/iOS/Example/App/Main/resource/localized/en.lproj/DemoLocalized.strings index 5e31bdb1b..3fef35169 100644 --- a/iOS/Example/App/Main/resource/localized/en.lproj/DemoLocalized.strings +++ b/iOS/Example/App/Main/resource/localized/en.lproj/DemoLocalized.strings @@ -25,4 +25,5 @@ "Demo.TUIRoomKit.raise.speaker" = "Raise hand Speaker Room"; "Demo.TUIRoomKit.video.conference" = "`s quick meeting"; "Demo.TUIRoomKit.prepareSetting" = "Prepare Settings"; -"Demo.TUIRoomKit.tip" = "Tencent Cloud TUIRoomKit"; +"Demo.TUIRoomKit.video.conferencing" = "Multi-party video conferencing"; +"Demo.TUIRoomKit.generating.roomId" = "Generating room number, please try again later"; diff --git a/iOS/Example/App/Main/resource/localized/zh-Hans.lproj/DemoLocalized.strings b/iOS/Example/App/Main/resource/localized/zh-Hans.lproj/DemoLocalized.strings index c8fa59386..ccd485b51 100644 --- a/iOS/Example/App/Main/resource/localized/zh-Hans.lproj/DemoLocalized.strings +++ b/iOS/Example/App/Main/resource/localized/zh-Hans.lproj/DemoLocalized.strings @@ -25,4 +25,5 @@ "Demo.TUIRoomKit.raise.speaker" = "举手发言房间"; "Demo.TUIRoomKit.video.conference" = "的快速会议"; "Demo.TUIRoomKit.prepareSetting" = "进房前设置"; -"Demo.TUIRoomKit.tip" = "腾讯云 TUIRoomKit"; +"Demo.TUIRoomKit.video.conferencing" = "多人视频会议"; +"Demo.TUIRoomKit.generating.roomId" = "正在生成房间号,请稍后重试"; diff --git a/iOS/Example/App/SceneDelegate.swift b/iOS/Example/App/SceneDelegate.swift index 363e40c23..d8aa4ecf1 100644 --- a/iOS/Example/App/SceneDelegate.swift +++ b/iOS/Example/App/SceneDelegate.swift @@ -18,7 +18,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.backgroundColor = UIColor.white let loginVC = TRTCLoginViewController() - let nav = UINavigationController(rootViewController: loginVC) + let nav = RoomNavigationController(rootViewController: loginVC) window?.rootViewController = nav window?.makeKeyAndVisible() } diff --git a/iOS/Example/DemoApp.xcodeproj/project.pbxproj b/iOS/Example/DemoApp.xcodeproj/project.pbxproj index 6277e7c89..dcb2bad6a 100644 --- a/iOS/Example/DemoApp.xcodeproj/project.pbxproj +++ b/iOS/Example/DemoApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -34,6 +34,7 @@ 7D93C86D2A80D0F80062B2CC /* EnterRoomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D93C86C2A80D0F80062B2CC /* EnterRoomViewController.swift */; }; 7D93C86F2A80D3B40062B2CC /* ListCellItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D93C86E2A80D3B40062B2CC /* ListCellItemView.swift */; }; 7D93C8722A80DE280062B2CC /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D93C8712A80DE280062B2CC /* String+Extension.swift */; }; + 7D9FC0D72B2C97EB0029F834 /* RoomNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D9FC0D62B2C97EB0029F834 /* RoomNavigationController.swift */; }; C18CE615279005D10071C120 /* ReplayKitLocalized.m in Sources */ = {isa = PBXBuildFile; fileRef = C18CE607279005D10071C120 /* ReplayKitLocalized.m */; }; C18CE616279005D10071C120 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C18CE608279005D10071C120 /* InfoPlist.strings */; }; C18CE617279005D10071C120 /* ReplayKitLocalized.strings in Resources */ = {isa = PBXBuildFile; fileRef = C18CE60A279005D10071C120 /* ReplayKitLocalized.strings */; }; @@ -116,6 +117,7 @@ 7D93C86C2A80D0F80062B2CC /* EnterRoomViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterRoomViewController.swift; sourceTree = ""; }; 7D93C86E2A80D3B40062B2CC /* ListCellItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCellItemView.swift; sourceTree = ""; }; 7D93C8712A80DE280062B2CC /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; + 7D9FC0D62B2C97EB0029F834 /* RoomNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomNavigationController.swift; sourceTree = ""; }; 906FB864AA4AD0E6330C2287 /* Pods_TXReplayKit_Screen.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TXReplayKit_Screen.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 90F2D7D94B9B392568BE9C57 /* Pods-TXReplayKit_Screen.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TXReplayKit_Screen.release.xcconfig"; path = "Target Support Files/Pods-TXReplayKit_Screen/Pods-TXReplayKit_Screen.release.xcconfig"; sourceTree = ""; }; 9A118A5B5DE110B8217C48CB /* Pods-DemoApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DemoApp.release.xcconfig"; path = "Target Support Files/Pods-DemoApp/Pods-DemoApp.release.xcconfig"; sourceTree = ""; }; @@ -262,6 +264,7 @@ 7D93C8502A80C9500062B2CC /* Component */ = { isa = PBXGroup; children = ( + 7D9FC0D62B2C97EB0029F834 /* RoomNavigationController.swift */, 7D93C8512A80C9780062B2CC /* RoomTypeView.swift */, 7D93C86E2A80D3B40062B2CC /* ListCellItemView.swift */, ); @@ -643,6 +646,7 @@ FA3A4AC12A543E9B0061C7DC /* RoomFileBroswerCell.swift in Sources */, 7D5B0C9D2A3AB75600CC1585 /* UserAgreementViewController+UI.swift in Sources */, 7D93C85D2A80CB3D0062B2CC /* ListCellItemData.swift in Sources */, + 7D9FC0D72B2C97EB0029F834 /* RoomNavigationController.swift in Sources */, 7D5B0C9B2A3AB75600CC1585 /* TRTCRegisterRootView.swift in Sources */, 7D93C86B2A80D0E50062B2CC /* CreateRoomViewController.swift in Sources */, FA3A4ABF2A543A660061C7DC /* RoomFileBroswerModel.swift in Sources */, diff --git a/iOS/Example/Login/TRTCLoginViewController.swift b/iOS/Example/Login/TRTCLoginViewController.swift index 101ed82cd..1fe439ac8 100644 --- a/iOS/Example/Login/TRTCLoginViewController.swift +++ b/iOS/Example/Login/TRTCLoginViewController.swift @@ -15,6 +15,14 @@ class TRTCLoginViewController: UIViewController { let loading = UIActivityIndicatorView() + override var shouldAutorotate: Bool { + return false + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() view.bringSubviewToFront(loading) diff --git a/iOS/Example/Login/TRTCRegisterViewController.swift b/iOS/Example/Login/TRTCRegisterViewController.swift index d04b1014a..f63522053 100644 --- a/iOS/Example/Login/TRTCRegisterViewController.swift +++ b/iOS/Example/Login/TRTCRegisterViewController.swift @@ -16,6 +16,14 @@ import TUICore class TRTCRegisterViewController: UIViewController { let loading = UIActivityIndicatorView(style: .large) + override var shouldAutorotate: Bool { + return false + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } + override func viewDidLoad() { super.viewDidLoad() TUICSToastManager.setDefaultPosition(TUICSToastPositionBottom) diff --git a/iOS/Example/Podfile b/iOS/Example/Podfile index 5200e979d..e9c315769 100644 --- a/iOS/Example/Podfile +++ b/iOS/Example/Podfile @@ -12,7 +12,7 @@ def tool pod 'Alamofire' pod 'TUICore' pod 'TUIChat' - pod 'TUIRoomEngine','1.6.1' + pod 'TUIRoomEngine','1.7.0' pod 'TXLiteAVSDK_TRTC' pod 'TXAppBasic' pod 'TIMCommon' @@ -47,6 +47,22 @@ post_install do |installer| config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' end end + xcode_version = `xcrun xcodebuild -version | grep Xcode | cut -d' ' -f2`.to_f + if xcode_version >= 15 + xcconfig_path = config.base_configuration_reference.real_path + xcconfig = File.read(xcconfig_path) + if xcconfig.include?("OTHER_LDFLAGS") == false + xcconfig = xcconfig + "\n" + 'OTHER_LDFLAGS = $(inherited) "-ld64"' + else + if xcconfig.include?("OTHER_LDFLAGS = $(inherited)") == false + xcconfig = xcconfig.sub("OTHER_LDFLAGS", "OTHER_LDFLAGS = $(inherited)") + end + if xcconfig.include?("-ld64") == false + xcconfig = xcconfig.sub("OTHER_LDFLAGS = $(inherited)", 'OTHER_LDFLAGS = $(inherited) "-ld64"') + end + end + File.open(xcconfig_path, "w") { |file| file << xcconfig } + end end end end diff --git a/iOS/TUIRoomKit/Resources/Localized/ar.lproj/TUIRoomKitLocalized.strings b/iOS/TUIRoomKit/Resources/Localized/ar.lproj/TUIRoomKitLocalized.strings index 56e498340..dc18086fd 100644 --- a/iOS/TUIRoomKit/Resources/Localized/ar.lproj/TUIRoomKitLocalized.strings +++ b/iOS/TUIRoomKit/Resources/Localized/ar.lproj/TUIRoomKitLocalized.strings @@ -21,6 +21,7 @@ "TUIRoom.sure.destroy.room" = "هل أنت متأكد أنك تريد إنهاء الاجتماع؟"; "TUIRoom.sure.leave.room" = "هل أنت متأكد أنك تريد مغادرة الغرفة"; +"TUIRoom.wait" = "يتمسك"; "TUIRoom.destroy.room.cancel" = "يتمسك"; "TUIRoom.ok" = "يتأكد"; "TUIRoom.cancel" = "يلغي"; @@ -71,7 +72,7 @@ "TUIRoom.leave.seat" = "انزل"; "TUIRoom.hand.down" = "بدون بذل الكثير من الجهد"; "TUIRoom.invite.to.speak" = "يدعوك المضيف للتحدث على المسرح"; -"TUIRoom.agree.to.speak" = "يدعوك المضيف للتحدث على المسرح، ويمكنك تشغيل الكاميرا والميكروفون بعد اعتلاء المسرح"; +"TUIRoom.agree.to.speak" = "يدعوك المضيف للتحدث على خشبة المسرح. وبمجرد صعودك على خشبة المسرح، يمكنك تشغيل الكاميرا وإلغاء كتم صوتك."; "TUIRoom.decline" = "يرفض"; "TUIRoom.agree" = "يوافق"; "TUIRoom.quick.meeting" = "اجتماع سريع"; @@ -153,7 +154,8 @@ "TUIRoom.share.off" = "التوقف عن المشاركة"; "TUIRoom.unfold" = "يوسع"; "TUIRoom.drop" = "وضع بعيدا"; -"TUIRoom.leave.room.tip" = "إذا كنت لا ترغب في إنهاء الغرفة، فقم بتعيين مضيف جديد قبل مغادرة الغرفة"; +"TUIRoom.appoint.owner" = "إذا كنت لا ترغب في إنهاء الغرفة، فقم بتعيين مضيف جديد قبل مغادرة الغرفة"; +"TUIRoom.leave.room.tip" = "هل أنت متأكد أنك تريد مغادرة الغرفة؟"; "TUIRoom.room.type" = "نوع الغرفة"; "TUIRoom.more" = "أكثر"; "TUIRoom.inviteMember" = "دعوة أعضاء"; @@ -181,3 +183,4 @@ "TUIRoom.take.seat.invitation.timeout" = "انتهت مهلة الدعوة للانضمام إلى ماي."; "TUIRoom.open.video.invitation.timeout" = "انتهت مهلة الدعوة لبدء الفيديو"; "TUIRoom.open.audio.invitation.timeout" = "انتهت مهلة الدعوة لتمكين الصوت"; +"TUIRoom.sure.kick.out" = "هل تريد نقل xx خارج الغرفة؟"; diff --git a/iOS/TUIRoomKit/Resources/Localized/en.lproj/TUIRoomKitLocalized.strings b/iOS/TUIRoomKit/Resources/Localized/en.lproj/TUIRoomKitLocalized.strings index ba946268b..ebb7e98dd 100644 --- a/iOS/TUIRoomKit/Resources/Localized/en.lproj/TUIRoomKitLocalized.strings +++ b/iOS/TUIRoomKit/Resources/Localized/en.lproj/TUIRoomKitLocalized.strings @@ -22,6 +22,7 @@ "TUIRoom.input.room.num" = "Enter a room ID"; "TUIRoom.sure.destroy.room" = "Are you sure you want to dismiss the meeting?"; "TUIRoom.sure.leave.room" = "Are you sure you want to leave the room?"; +"TUIRoom.wait" = "Wait"; "TUIRoom.destroy.room.cancel" = "Cancel"; "TUIRoom.ok" = "OK"; "TUIRoom.cancel" = "Cancel"; @@ -88,7 +89,7 @@ "TUIRoom.leave.seat" = "Step down"; "TUIRoom.hand.down" = "Hands down"; "TUIRoom.invite.to.speak" = "The host invites you to speak on stage"; -"TUIRoom.agree.to.speak" = "The host invites you to speak on stage, and you can turn on the camera and microphone after taking the stage"; +"TUIRoom.agree.to.speak" = "The host invites you to speak on stage. Once on stage, you can turn on the camera and unmute yourself."; "TUIRoom.decline" = "Decline"; "TUIRoom.agree" = "Agree"; "TUIRoom.quick.meeting" = "Quick meeting"; @@ -97,8 +98,8 @@ "TUIRoom.me" = "Me"; "TUIRoom.role.owner" = "Owner"; "TUIRoom.invite.members" = "Invite members to stage"; -"TUIRoom.agree.seat" = "Agree to come on stage"; -"TUIRoom.disagree.seat" = "Disagree to come on stage"; +"TUIRoom.agree.seat" = "Agree take seat"; +"TUIRoom.disagree.seat" = "Disagree take seat"; "TUIRoom.code" = "Room QR code"; "TUIRoom.scan.code" = "Scan the code to enter the room"; "TUIRoom.save.into.album" = "Save into the album"; @@ -106,7 +107,8 @@ "TUIRoom.invite.turn.on.video" = "The host invites you to turn on the video"; "TUIRoom.dismiss.meeting.Title" = "If you don't want to end the meeting"; "TUIRoom.appoint.new.host" = "Please appoint a new host before leaving the meeting"; -"TUIRoom.leave.room.tip" = "If you do not want to end the room, please appoint a new moderator before leaving the room"; +"TUIRoom.appoint.owner" = "If you do not want to end the room, please appoint a new moderator before leaving the room"; +"TUIRoom.leave.room.tip" = "Are you sure you want to leave the room"; "TUIRoom.leave.meeting" = "Leave meeting"; "TUIRoom.leave.room" = "Leave Room"; "TUIRoom.dismiss.meeting" = "Dismiss metting"; @@ -159,15 +161,14 @@ "TUIRoom.fail.microphone.permission.Enable" = "Authorize Now"; "TUIRoom.fail.camera.permission.Title" = "No access to camera"; "TUIRoom.fail.camera.permission.Tips" = "Unable to use the video function, click \"Authorize Now\" to open the camera permission."; -"TUIRoom.fail.camera.permission.Later" = "Later"; -"TUIRoom.fail.camera.permission.Enable" = "Authorize Now"; +"TUIRoom.fail.permission.Later" = "Later"; +"TUIRoom.fail.permission.Enable" = "Authorize Now"; "TUIRoom.device.set" = "Meeting Settings"; "TUIRoom.mic.set" = "Join the meeting and start the audio"; "TUIRoom.camera.set" = "Join the conference and turn on the camera"; "TUIRoom.sharing.screen" = "You are sharing your screen"; "TUIRoom.stop.share.screen" = "Stop screen sharing"; "TUIRoom.screen.recording" = "Screen recording"; - "TUIRoom.toast.shareScreen.title" = "Share Screen"; "TUIRoom.toast.shareScreen.message" = "Stop TUIRoom screen sharing screen live?"; "TUIRoom.toast.shareScreen.cancel" = "Cancel"; @@ -189,3 +190,4 @@ "TUIRoom.take.seat.invitation.timeout" = "The invitation to take seat has timed out"; "TUIRoom.open.video.invitation.timeout" = "The invitation to start the video has timed out"; "TUIRoom.open.audio.invitation.timeout" = "The invitation to start the audio has timed out"; +"TUIRoom.sure.kick.out" = "Do you want to move xx out of the room?"; diff --git a/iOS/TUIRoomKit/Resources/Localized/zh-Hans.lproj/TUIRoomKitLocalized.strings b/iOS/TUIRoomKit/Resources/Localized/zh-Hans.lproj/TUIRoomKitLocalized.strings index 6d79c8eff..1ebf4b84e 100644 --- a/iOS/TUIRoomKit/Resources/Localized/zh-Hans.lproj/TUIRoomKitLocalized.strings +++ b/iOS/TUIRoomKit/Resources/Localized/zh-Hans.lproj/TUIRoomKitLocalized.strings @@ -23,8 +23,8 @@ "TUIRoom.sure.destroy.room" = "你确定要结束会议吗?"; "TUIRoom.sure.leave.room" = "你确定要离开房间吗?"; -"TUIRoom.destroy.room.cancel" = "再等等"; -"TUIRoom.ok" = "确认"; +"TUIRoom.wait" = "再等等"; +"TUIRoom.ok" = "确定"; "TUIRoom.cancel" = "取消"; "TUIRoom.invite.join" = "您可以分享房间号或链接邀请更多人加入房间"; "TUIRoom.logout.member.list" = "成员列表"; @@ -86,7 +86,7 @@ "TUIRoom.leave.seat" = "下台"; "TUIRoom.hand.down" = "手放下"; "TUIRoom.invite.to.speak" = "主持人邀请您上台发言"; -"TUIRoom.agree.to.speak" = "主持人邀请您上台发言,上台后可以打开摄像头和麦克风"; +"TUIRoom.agree.to.speak" = "主持人邀请您上台发言,上台后可以打开摄像头和取消静音"; "TUIRoom.decline" = "拒绝"; "TUIRoom.agree" = "同意"; "TUIRoom.quick.meeting" = "快速会议"; @@ -104,7 +104,8 @@ "TUIRoom.invite.turn.on.video" = "主持人邀请您开启视频画面"; "TUIRoom.dismiss.meeting.Title" = "如果您不想结束会议"; "TUIRoom.appoint.new.host" = "请在离开会议前指定新的主持人"; -"TUIRoom.leave.room.tip" = "如果您不想结束房间,请在离开房间前指定新的主持人"; +"TUIRoom.appoint.owner" = "如果您不想结束房间,请在离开房间前指定新的主持人"; +"TUIRoom.leave.room.tip"= "您确定要离开房间吗"; "TUIRoom.leave.meeting" = "离开会议"; "TUIRoom.leave.room" = "离开房间"; "TUIRoom.dismiss.meeting" = "结束会议"; @@ -186,3 +187,4 @@ "TUIRoom.take.seat.invitation.timeout" = "上麦邀请已经超时"; "TUIRoom.open.video.invitation.timeout" = "开启视频的邀请已经超时"; "TUIRoom.open.audio.invitation.timeout" = "开启音频的邀请已经超时"; +"TUIRoom.sure.kick.out" = "是否将xx移出房间?"; diff --git a/iOS/TUIRoomKit/RoomExtension/Model/Manager/RoomManager.swift b/iOS/TUIRoomKit/RoomExtension/Model/Manager/RoomManager.swift index e7fbe1b6b..eeabc720f 100644 --- a/iOS/TUIRoomKit/RoomExtension/Model/Manager/RoomManager.swift +++ b/iOS/TUIRoomKit/RoomExtension/Model/Manager/RoomManager.swift @@ -66,6 +66,7 @@ class RoomManager { func enterRoom(roomId: String) { roomObserver.registerObserver() + engineManager.store.isImAccess = true engineManager.enterRoom(roomId: roomId, enableAudio: engineManager.store.isOpenMicrophone, enableVideo: engineManager.store.isOpenCamera, isSoundOnSpeaker: true) { [weak self] in guard let self = self else { return } diff --git a/iOS/TUIRoomKit/Source/Common/CGFloat+Extension.swift b/iOS/TUIRoomKit/Source/Common/CGFloat+Extension.swift index e0d1eef11..bb1673ea2 100644 --- a/iOS/TUIRoomKit/Source/Common/CGFloat+Extension.swift +++ b/iOS/TUIRoomKit/Source/Common/CGFloat+Extension.swift @@ -37,10 +37,10 @@ public let kDeviceSafeBottomHeight : CGFloat = { } }() private var width: CGFloat { - return isLandscape ? kScreenHeight : kScreenWidth + return min(kScreenHeight, kScreenWidth) } private var height: CGFloat { - return isLandscape ? kScreenWidth : kScreenHeight + return max(kScreenWidth, kScreenHeight) } extension CGFloat { diff --git a/iOS/TUIRoomKit/Source/Common/UIImage+RTL.swift b/iOS/TUIRoomKit/Source/Common/UIImage+RTL.swift index a79eae1cc..132f7ad05 100644 --- a/iOS/TUIRoomKit/Source/Common/UIImage+RTL.swift +++ b/iOS/TUIRoomKit/Source/Common/UIImage+RTL.swift @@ -16,7 +16,7 @@ extension UIImage { bitmap.translateBy(x: self.size.width / 2, y: self.size.height / 2) bitmap.scaleBy(x: -1.0, y: -1.0) bitmap.translateBy(x: -self.size.width / 2, y: -self.size.height / 2) - bitmap.draw(cgImage, in: CGRectMake(0, 0, self.size.width, self.size.height)) + bitmap.draw(cgImage, in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)) let image = UIGraphicsGetImageFromCurrentImageContext() return image } diff --git a/iOS/TUIRoomKit/Source/Model/EngineEventCenter.swift b/iOS/TUIRoomKit/Source/Model/EngineEventCenter.swift index b1e452a31..ac67d7367 100644 --- a/iOS/TUIRoomKit/Source/Model/EngineEventCenter.swift +++ b/iOS/TUIRoomKit/Source/Model/EngineEventCenter.swift @@ -59,6 +59,10 @@ class EngineEventCenter: NSObject { case onUserScreenCaptureStopped case onRequestReceived case onSendMessageForUserDisableChanged + case onRemoteUserEnterRoom + case onRemoteUserLeaveRoom + case onUserRoleChanged + case onSeatListChanged } enum RoomUIEvent: String { @@ -81,6 +85,7 @@ class EngineEventCenter: NSObject { case TUIRoomKitService_SetToolBarDelayHidden //设定工具栏是否3秒之后隐藏(参数:isDelay) case TUIRoomKitService_HiddenChatWindow //隐藏聊天窗口 case TUIRoomKitService_ShowExitRoomView //显示离开房间页面 + case TUIRoomKitService_RenewVideoSeatView //更新视频页面 } /// 注册UI响应相关监听事件 diff --git a/iOS/TUIRoomKit/Source/Model/EngineManager.swift b/iOS/TUIRoomKit/Source/Model/EngineManager.swift index f6426138e..7c7ddd704 100644 --- a/iOS/TUIRoomKit/Source/Model/EngineManager.swift +++ b/iOS/TUIRoomKit/Source/Model/EngineManager.swift @@ -38,7 +38,7 @@ class EngineManager: NSObject { let eventDispatcher = RoomEventDispatcher() return eventDispatcher }() - private let timeOutNumber: Double = 0 + private let timeOutNumber: Double = 10 private let rootRouter: RoomRouter = RoomRouter.shared private var isLoginEngine: Bool = false private let appGroupString: String = "com.tencent.TUIRoomTXReplayKit-Screen" @@ -98,35 +98,25 @@ class EngineManager: NSObject { } func exitRoom(onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) { - roomEngine.getTRTCCloud().stopAllRemoteView() roomEngine.exitRoom(syncWaiting: false) { [weak self] in guard let self = self else { return } self.destroyEngineManager() - EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ExitedRoom, param: ["isExited":true]) + EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ExitedRoom, param: [:]) onSuccess?() - } onError: { [weak self] code, message in - guard let self = self else { return } - self.destroyEngineManager() - EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ExitedRoom, param: ["isExited":false]) + } onError: { code, message in onError?(code, message) } - TRTCCloud.destroySharedIntance() } func destroyRoom(onSuccess: TUISuccessBlock? = nil, onError: TUIErrorBlock? = nil) { - roomEngine.getTRTCCloud().stopAllRemoteView() roomEngine.destroyRoom { [weak self] in guard let self = self else { return } self.destroyEngineManager() - EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DestroyedRoom, param: ["isDestroyed":true]) + EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DestroyedRoom, param: [:]) onSuccess?() - } onError: { [weak self] code, message in - guard let self = self else { return } - self.destroyEngineManager() - EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_DestroyedRoom, param: ["isDestroyed":false]) + } onError: { code, message in onError?(code, message) } - TRTCCloud.destroySharedIntance() } func destroyEngineManager() { @@ -239,20 +229,32 @@ class EngineManager: NSObject { onCancelled: TUIRequestCancelledBlock? = nil, onTimeout: TUIRequestTimeoutBlock? = nil, onError: TUIRequestErrorBlock? = nil) { - roomEngine.takeUserOnSeatByAdmin(-1, userId: userId, timeout: timeout) { requestId, userId in + store.extendedInvitationList.append(userId) + roomEngine.takeUserOnSeatByAdmin(-1, userId: userId, timeout: timeout) { [weak self] requestId, userId in + guard let self = self else { return } + self.store.extendedInvitationList.removeAll(where: { $0 == userId }) onAccepted?(requestId, userId) - } onRejected: { requestId, userId, message in - onRejected?(requestId, userId, message) - } onCancelled: { requestId, userId in + } onRejected: { [weak self] requestId, userId, message in + guard let self = self else { return } + self.store.extendedInvitationList.removeAll(where: { $0 == userId }) + onRejected?( requestId, userId, message) + } onCancelled: { [weak self] requestId, userId in + guard let self = self else { return } + self.store.extendedInvitationList.removeAll(where: { $0 == userId }) onCancelled?(requestId, userId) - } onTimeout: { requestId, userId in + } onTimeout: { [weak self] requestId, userId in + guard let self = self else { return } + self.store.extendedInvitationList.removeAll(where: { $0 == userId }) onTimeout?(requestId, userId) - } onError: { requestId, userId, code, message in + } onError: { [weak self] requestId, userId, code, message in + guard let self = self else { return } + self.store.extendedInvitationList.removeAll(where: { $0 == userId }) onError?(requestId, userId, code, message) } } func setAudioRoute(route: TRTCAudioRoute) { + store.audioSetting.isSoundOnSpeaker = route == .modeSpeakerphone roomEngine.getTRTCCloud().setAudioRoute(route) } @@ -524,6 +526,10 @@ class EngineManager: NSObject { func changeRaiseHandNoticeState(isShown: Bool) { store.isShownRaiseHandNotice = isShown } + + func setRemoteRenderParams(userId: String, streamType: TRTCVideoStreamType, params: TRTCRenderParams) { + roomEngine.getTRTCCloud().setRemoteRenderParams(userId, streamType: streamType, params: params) + } } // MARK: - Private @@ -688,6 +694,9 @@ extension EngineManager { } else { self.store.attendeeList = localUserList onSuccess() + if self.store.roomInfo.speechMode != .applySpeakAfterTakingSeat { + EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, param: [:]) + } EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewUserList, param: [:]) } } onError: { code, message in @@ -713,6 +722,7 @@ extension EngineManager { self.store.seatList = localSeatList onSuccess() EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewSeatList, param: [:]) + EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_RenewVideoSeatView, param: [:]) } onError: { code, message in onError(code, message) debugPrint("getSeatList:code:\(code),message:\(message)") @@ -756,14 +766,16 @@ extension EngineManager: TUIExtensionProtocol { 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":\(EngineManager.TUIRoomKitComponentValue), + "component":\(componentValue), "language":\(EngineManager.TUIRoomKitLanguageValue) } } diff --git a/iOS/TUIRoomKit/Source/Model/RoomEventDispatcher.swift b/iOS/TUIRoomKit/Source/Model/RoomEventDispatcher.swift index 285fcee35..1387e0927 100644 --- a/iOS/TUIRoomKit/Source/Model/RoomEventDispatcher.swift +++ b/iOS/TUIRoomKit/Source/Model/RoomEventDispatcher.swift @@ -65,14 +65,29 @@ extension RoomEventDispatcher: TUIRoomObserver { // MARK: - 房间内用户事件回调 func onRemoteUserEnterRoom(roomId: String, userInfo: TUIUserInfo) { remoteUserEnterRoom(roomId: roomId, userInfo: userInfo) + let param = [ + "roomId" : roomId, + "userInfo" : userInfo, + ] as [String : Any] + EngineEventCenter.shared.notifyEngineEvent(event: .onRemoteUserEnterRoom, param: param) } func onRemoteUserLeaveRoom(roomId: String, userInfo: TUIUserInfo) { remoteUserLeaveRoom(roomId: roomId, 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) { @@ -108,6 +123,12 @@ extension RoomEventDispatcher: TUIRoomObserver { 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) { @@ -153,6 +174,7 @@ extension RoomEventDispatcher { //判断自己是否下麦 if leftList.first(where: { $0.userId == currentUser.userId }) != nil { currentUser.isOnSeat = false + store.audioSetting.isMicOpened = false if currentUser.hasScreenStream { //如果正在进行屏幕共享,要把屏幕共享关闭。 engineManager.stopScreenCapture() } diff --git a/iOS/TUIRoomKit/Source/Model/RoomStore.swift b/iOS/TUIRoomKit/Source/Model/RoomStore.swift index 60de096de..168804802 100644 --- a/iOS/TUIRoomKit/Source/Model/RoomStore.swift +++ b/iOS/TUIRoomKit/Source/Model/RoomStore.swift @@ -24,6 +24,8 @@ class RoomStore: NSObject { var isEnteredRoom: Bool = false //是否已经进入房间 var timeStampOnEnterRoom: Int = 0 //进入会议的时间戳 var isShowRoomMainViewAutomatically: Bool = true //true 调用createRoom或者enterRoom会自动进入主界面; false 需要调用 showRoomMainView 才能进入主界面。 + var extendedInvitationList : [String] = [] //已经发出邀请的用户列表 + var isImAccess: Bool = false //是否由IM进入的TUIRoomKit private let openCameraKey = "isOpenCamera" private let openMicrophoneKey = "isOpenMicrophone" private let shownRaiseHandNoticeKey = "isShownRaiseHandNotice" @@ -65,7 +67,7 @@ class RoomStore: NSObject { } func initialRoomCurrentUser() { - EngineManager.createInstance().getUserInfo(currentUser.userId) { [weak self] userInfo in + EngineManager.createInstance().getUserInfo(TUILogin.getUserID() ?? "") { [weak self] userInfo in guard let self = self else { return } guard let userInfo = userInfo else { return } self.currentUser.update(userInfo: userInfo) @@ -73,4 +75,8 @@ class RoomStore: NSObject { debugPrint("getUserInfo,code:\(code),message:\(message)") } } + + deinit { + debugPrint("self:\(self),deinit") + } } diff --git a/iOS/TUIRoomKit/Source/Model/VideoSeatItem.swift b/iOS/TUIRoomKit/Source/Model/VideoSeatItem.swift index 1bd4d213e..1dfd50c63 100644 --- a/iOS/TUIRoomKit/Source/Model/VideoSeatItem.swift +++ b/iOS/TUIRoomKit/Source/Model/VideoSeatItem.swift @@ -18,50 +18,35 @@ class VideoSeatItem: Equatable { static func == (lhs: VideoSeatItem, rhs: VideoSeatItem) -> Bool { return (lhs.userId == rhs.userId) && (lhs.type == rhs.type) } - + private var itemType: VideoSeatItemType = .original private var videoStreamType: TUIVideoStreamType = .cameraStream - private var userInfo: TUIUserInfo - var isPlaying: Bool = false + private var userInfo: UserEntity var audioVolume: Int = 0 - weak var boundCell: TUIVideoSeatCell? var isSelf: Bool { - return userInfo.userId == TUIRoomEngine.getSelfInfo().userId + return userInfo.userId == EngineManager.createInstance().store.currentUser.userId } var streamType: TUIVideoStreamType { return videoStreamType } - + var type: VideoSeatItemType { return itemType } - + var userId: String { return userInfo.userId } - - var isRoomOwner: Bool { - return userRole == .roomOwner - } - + var userName: String { return userInfo.userName } - + var avatarUrl: String { return userInfo.avatarUrl } - - var userRole: TUIRole { - set { - userInfo.userRole = newValue - } - get { - return userInfo.userRole - } - } - + var hasAudioStream: Bool { set { userInfo.hasAudioStream = newValue @@ -70,7 +55,7 @@ class VideoSeatItem: Equatable { return userInfo.hasAudioStream } } - + var hasVideoStream: Bool { set { userInfo.hasVideoStream = newValue @@ -79,7 +64,7 @@ class VideoSeatItem: Equatable { return userInfo.hasVideoStream } } - + var hasScreenStream: Bool { set { userInfo.hasScreenStream = newValue @@ -88,31 +73,26 @@ class VideoSeatItem: Equatable { return userInfo.hasScreenStream } } - + var isHasVideoStream: Bool { return hasVideoStream || hasScreenStream } - init(userId: String) { - userInfo = TUIUserInfo() - userInfo.userId = userId - } - - init(userInfo: TUIUserInfo) { + init(userInfo: UserEntity) { self.userInfo = userInfo } - - func updateUserInfo(_ userInfo: TUIUserInfo) { + + func updateUserInfo(_ userInfo: UserEntity) { self.userInfo = userInfo } - + func cloneShare() -> VideoSeatItem { let item = VideoSeatItem(userInfo: userInfo) item.videoStreamType = .screenStream item.itemType = .share return item } - + func updateStreamType(streamType: TUIVideoStreamType) { if videoStreamType != .screenStream { videoStreamType = streamType diff --git a/iOS/TUIRoomKit/Source/View/Component/ListCellItemView.swift b/iOS/TUIRoomKit/Source/View/Component/ListCellItemView.swift index ef3668c96..e0565df7a 100644 --- a/iOS/TUIRoomKit/Source/View/Component/ListCellItemView.swift +++ b/iOS/TUIRoomKit/Source/View/Component/ListCellItemView.swift @@ -114,18 +114,18 @@ class ListCellItemView: UIView { make.height.equalTo(20.scale375()) } - sliderLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().offset(85.scale375()) - make.trailing.equalToSuperview().offset(-160.scale375()) - make.centerY.equalToSuperview() - } - 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() diff --git a/iOS/TUIRoomKit/Source/View/Page/RoomMainView.swift b/iOS/TUIRoomKit/Source/View/Page/RoomMainView.swift index e91a24e2b..c316aa748 100644 --- a/iOS/TUIRoomKit/Source/View/Page/RoomMainView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/RoomMainView.swift @@ -139,7 +139,7 @@ class RoomMainView: UIView { bottomView.snp.remakeConstraints { make in make.leading.equalTo(safeAreaLayoutGuide.snp.leading) make.trailing.equalTo(safeAreaLayoutGuide.snp.trailing) - make.height.equalTo(bottomView.isUnfold ? 130.scale375() : 60.scale375()) + make.height.equalTo(bottomView.isUnfold ? bottomView.unfoldHeight : bottomView.packUpHeight) if isLandscape { make.bottom.equalToSuperview().offset(-layout.bottomViewLandscapeSpace) } else { @@ -165,22 +165,8 @@ extension RoomMainView: RoomMainViewResponder { muteAudioButton.isSelected = isSelected } - func showAlert(title: String?, message: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) { - let alertVC = UIAlertController(title: title, - message: message, - preferredStyle: .alert) - let sureTitle: String = declineBlock != nil ? .agreeText : .alertOkText - let sureAction = UIAlertAction(title: sureTitle, style: .default) { _ in - sureBlock?() - } - alertVC.addAction(sureAction) - if let declineBlock = declineBlock { - let declineAction = UIAlertAction(title: .declineText, style: .destructive) { _ in - declineBlock() - } - alertVC.addAction(declineAction) - } - RoomRouter.shared.presentAlert(alertVC) + 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 makeToast(text: String) { @@ -231,15 +217,3 @@ extension RoomMainView: RoomMainViewResponder { perform(#selector(hideToolBar),with: nil,afterDelay: delayDisappearanceTime) } } - -private extension String { - static var alertOkText: String { - localized("TUIRoom.ok") - } - static var declineText: String { - localized("TUIRoom.decline") - } - static var agreeText: String { - localized("TUIRoom.agree") - } -} diff --git a/iOS/TUIRoomKit/Source/View/Page/RoomRouter.swift b/iOS/TUIRoomKit/Source/View/Page/RoomRouter.swift index 2cbb22c2e..33a059a14 100644 --- a/iOS/TUIRoomKit/Source/View/Page/RoomRouter.swift +++ b/iOS/TUIRoomKit/Source/View/Page/RoomRouter.swift @@ -174,8 +174,22 @@ class RoomRouter: NSObject { } } - func presentAlert(_ alertController: UIAlertController) { - getCurrentWindowViewController()?.present(alertController, animated: true) + class func presentAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) { + 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 sureAction = UIAlertAction(title: sureTitle, style: .default) { _ in + sureBlock?() + } + alertVC.addAction(sureAction) + shared.getCurrentWindowViewController()?.present(alertVC, animated: true) } class func makeToast(toast: String) { diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/BottomNavigationBar/BottomView.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/BottomNavigationBar/BottomView.swift index 42f07fdbe..39db65aec 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/BottomNavigationBar/BottomView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/BottomNavigationBar/BottomView.swift @@ -17,6 +17,8 @@ class BottomView: UIView { var dropButtonItem: BottomItemView? var recordButtonItem: BottomItemView? var isUnfold: Bool = false //是否展开 + let unfoldHeight = Float(130.scale375Height()) + let packUpHeight = Float(68.scale375Height()) let baseButtonMenuView: UIStackView = { let view = UIStackView() @@ -168,7 +170,7 @@ class BottomView: UIView { func activateConstraints() { backgroundView.snp.makeConstraints { make in make.bottom.leading.trailing.equalToSuperview() - make.height.equalTo(60.scale375()) + make.height.equalTo(packUpHeight) } let width = min(kScreenWidth, kScreenHeight) buttonMenuView.snp.makeConstraints { make in @@ -198,6 +200,10 @@ class BottomView: UIView { } extension BottomView: BottomViewModelResponder { + func showStopShareScreenAlert(sureBlock: (() -> ())?) { + RoomRouter.presentAlert(title: .toastTitleText, message: .toastMessageText, sureTitle: .toastStopText, declineTitle: .toastCancelText, sureBlock: sureBlock, declineBlock: nil) + } + func updateStackView(item: ButtonItemData, index: Int) { guard viewArray.count > index else { return } viewArray[index].setupViewState(item: item) @@ -211,7 +217,7 @@ extension BottomView: BottomViewModelResponder { UIView.animate(withDuration: 0.3) { [weak self] () in guard let self = self else { return } self.snp.updateConstraints { make in - make.height.equalTo(isUnfold ? 130.scale375() : 60.scale375()) + make.height.equalTo(isUnfold ? self.unfoldHeight : self.packUpHeight) } self.superview?.layoutIfNeeded() } completion: { _ in @@ -266,7 +272,7 @@ private extension String { } static var destroyRoomCancelTitle: String { - localized("TUIRoom.destroy.room.cancel") + localized("TUIRoom.wait") } static var logoutOkText: String { @@ -296,4 +302,17 @@ private extension String { static var memberText: String { localized("TUIRoom.conference.member") } + + static var toastTitleText: String { + localized("TUIRoom.toast.shareScreen.title") + } + static var toastMessageText: String { + localized("TUIRoom.toast.shareScreen.message") + } + static var toastCancelText: String { + localized("TUIRoom.toast.shareScreen.cancel") + } + static var toastStopText: String { + localized("TUIRoom.toast.shareScreen.stop") + } } diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/Dialog/ExitRoomView.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/Dialog/ExitRoomView.swift index 8b8c13450..fa0725378 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/Dialog/ExitRoomView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/Dialog/ExitRoomView.swift @@ -31,14 +31,14 @@ class ExitRoomView: UIView { return view }() - let titleLabel: UILabel = { + lazy var titleLabel: UILabel = { let label = UILabel() - label.text = .leaveRoomTipText label.textColor = UIColor(0x7C85A6) label.font = UIFont(name: "PingFangSC-Regular", size: 12) label.textAlignment = .center label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping + label.text = viewModel.isShownExitRoomButton() && viewModel.isShownLeaveRoomButton() ? .appointOwnerText : .leaveRoomTipText return label }() @@ -55,7 +55,6 @@ class ExitRoomView: UIView { button.setTitleColor(UIColor(0x006CFF), for: .normal) button.backgroundColor = UIColor(0x17181F) button.isEnabled = true - button.addTarget(self, action: #selector(leaveRoomAction), for: .touchUpInside) return button }() @@ -72,7 +71,6 @@ class ExitRoomView: UIView { button.setTitleColor(UIColor(0xE5395C), for: .normal) button.backgroundColor = UIColor(0x17181F) button.isEnabled = true - button.addTarget(self, action: #selector(exitRoomAction), for: .touchUpInside) return button }() @@ -92,9 +90,9 @@ class ExitRoomView: UIView { activateConstraints() bindInteraction() isViewReady = true - exitRoomButton.isHidden = currentUser.userId != roomInfo.ownerId - leaveRoomButton.isHidden = !viewModel.isShowLeaveRoomButton() - boundary2View.isHidden = currentUser.userId != roomInfo.ownerId || !viewModel.isShowLeaveRoomButton() + exitRoomButton.isHidden = !viewModel.isShownExitRoomButton() + leaveRoomButton.isHidden = !viewModel.isShownLeaveRoomButton() + boundary2View.isHidden = !viewModel.isShownExitRoomButton() || !viewModel.isShownLeaveRoomButton() } func constructViewHierarchy() { @@ -109,7 +107,7 @@ class ExitRoomView: UIView { func activateConstraints() { let titleLabelHeight = 67.scale375Height() - let leaveRoomButtonHeight = viewModel.isShowLeaveRoomButton() ? 57.scale375Height() : 0 + let leaveRoomButtonHeight = viewModel.isShownLeaveRoomButton() ? 57.scale375Height() : 0 let exitRoomButtonHeight = currentUser.userId == roomInfo.ownerId ? 57.scale375Height() : 0 let space = 20.scale375Height() let contentViewHeight = titleLabelHeight + leaveRoomButtonHeight + exitRoomButtonHeight + space @@ -149,6 +147,8 @@ class ExitRoomView: UIView { } func bindInteraction() { + leaveRoomButton.addTarget(self, action: #selector(leaveRoomAction), for: .touchUpInside) + exitRoomButton.addTarget(self, action: #selector(exitRoomAction), for: .touchUpInside) contentView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight) panelControl.addTarget(self, action: #selector(clickBackgroundView), for: .touchUpInside) } @@ -195,10 +195,19 @@ class ExitRoomView: UIView { } } +extension ExitRoomView: ExitRoomViewModelResponder { + func makeToast(message: String) { + makeToast(message) + } +} + private extension String { static var leaveRoomTipText: String { localized("TUIRoom.leave.room.tip" ) } + static var appointOwnerText: String { + localized("TUIRoom.appoint.owner" ) + } static var leaveRoomText: String { localized("TUIRoom.leave.room") } diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/RaiseHandControlPanel/RaiseHandApplicationListView.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/RaiseHandControlPanel/RaiseHandApplicationListView.swift index b6d8d1b73..b8b4b4233 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/RaiseHandControlPanel/RaiseHandApplicationListView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/RaiseHandControlPanel/RaiseHandApplicationListView.swift @@ -290,6 +290,7 @@ class ApplyTableCell: UITableViewCell { button.setTitle(.agreeSeatText, for: .normal) button.setTitleColor(.white, for: .normal) button.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .regular) + button.titleLabel?.adjustsFontSizeToFitWidth = true button.layer.cornerRadius = 6 button.clipsToBounds = true return button @@ -300,6 +301,7 @@ class ApplyTableCell: UITableViewCell { button.backgroundColor = .red button.setTitle(.disagreeSeatText, for: .normal) button.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .regular) + button.titleLabel?.adjustsFontSizeToFitWidth = true button.setTitleColor(.white, for: .normal) button.layer.cornerRadius = 6 button.clipsToBounds = true diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/TransferOwnerControlPanel/TransferMasterView.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/TransferOwnerControlPanel/TransferMasterView.swift index e38d9777a..c548ebf13 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/TransferOwnerControlPanel/TransferMasterView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/TransferOwnerControlPanel/TransferMasterView.swift @@ -205,6 +205,10 @@ extension TransferMasterView: UITableViewDelegate { } extension TransferMasterView: TransferMasterViewResponder { + func makeToast(message: String) { + makeToast(message) + } + func reloadTransferMasterTableView() { guard !isSearching else { return } attendeeList = viewModel.attendeeList diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/UserControlPanel/UserListManagerView.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/UserControlPanel/UserListManagerView.swift index a31cd12ff..8f5027b52 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/UserControlPanel/UserListManagerView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/UserControlPanel/UserListManagerView.swift @@ -172,6 +172,10 @@ class UserListManagerView: UIView { } extension UserListManagerView: UserListManagerViewEventResponder { + func showKickOutAlert(title: String, sureAction: (() -> ())?) { + RoomRouter.presentAlert(title: title, message: nil, sureTitle: .alertOkText, declineTitle: .cancelText, sureBlock: sureAction, declineBlock: nil) + } + func updateUI(items: [ButtonItemData]) { setupViewState() viewArray.forEach { view in @@ -203,13 +207,7 @@ extension UserListManagerView: UserListManagerViewEventResponder { } func showTransferredRoomOwnerAlert() { - let alertVC = UIAlertController(title: .haveTransferredMasterText, - message: nil, - preferredStyle: .alert) - let sureAction = UIAlertAction(title: .alertOkText, style: .cancel) { _ in - } - alertVC.addAction(sureAction) - RoomRouter.shared.presentAlert(alertVC) + RoomRouter.presentAlert(title: .haveTransferredMasterText, message: nil, sureTitle: .alertOkText, declineTitle: nil, sureBlock: nil, declineBlock: nil) } func setUserListManagerViewHidden(isHidden: Bool) { @@ -227,5 +225,8 @@ private extension String { static var haveTransferredMasterText: String { localized("TUIRoom.have.transferred.master") } + static var cancelText: String { + localized("TUIRoom.cancel") + } } diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/UserControlPanel/UserListView.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/UserControlPanel/UserListView.swift index 804f6dfcb..972f5dc1b 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/UserControlPanel/UserListView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/UserControlPanel/UserListView.swift @@ -100,6 +100,12 @@ class UserListView: UIView { return button }() + let bottomView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(0x17181F) + return view + }() + lazy var userListTableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.separatorStyle = .none @@ -141,9 +147,10 @@ class UserListView: UIView { addSubview(searchBar) addSubview(inviteButton) addSubview(userListTableView) - addSubview(muteAllAudioButton) - addSubview(muteAllVideoButton) - addSubview(moreFunctionButton) + addSubview(bottomView) + bottomView.addSubview(muteAllAudioButton) + bottomView.addSubview(muteAllVideoButton) + bottomView.addSubview(moreFunctionButton) addSubview(blurView) addSubview(userListManagerView) addSubview(searchControl) @@ -168,26 +175,30 @@ class UserListView: UIView { make.trailing.equalToSuperview().offset(-16.scale375()) make.height.equalTo(36.scale375()) } + bottomView.snp.makeConstraints { make in + make.leading.trailing.bottom.equalToSuperview() + make.height.equalTo(84.scale375Height()) + } userListTableView.snp.makeConstraints { make in make.leading.equalToSuperview().offset(16.scale375()) make.trailing.equalToSuperview().offset(-16.scale375()) make.top.equalToSuperview().offset(127.scale375()) - make.bottom.equalToSuperview() + make.bottom.equalTo(bottomView.snp.top) } muteAllAudioButton.snp.makeConstraints { make in - make.bottom.equalToSuperview().offset(-34.scale375()) + 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.bottom.equalToSuperview().offset(-34.scale375()) + 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.bottom.equalToSuperview().offset(-34.scale375()) + make.top.equalToSuperview().offset(10.scale375Height()) make.leading.equalToSuperview().offset(250.scale375()) make.width.equalTo(108.scale375()) make.height.equalTo(40.scale375()) @@ -396,6 +407,7 @@ class UserListCell: UITableViewCell { 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 }() @@ -453,8 +465,6 @@ class UserListCell: UITableViewCell { make.centerY.equalTo(self.avatarImageView) } inviteStageButton.snp.makeConstraints { make in - make.width.equalTo(62.scale375()) - make.height.equalTo(24.scale375Height()) make.trailing.equalToSuperview() make.centerY.equalTo(self.avatarImageView) } @@ -512,18 +522,13 @@ class UserListCell: UITableViewCell { muteAudioButton.isSelected = !item.hasAudioStream muteVideoButton.isSelected = !item.hasVideoStream //判断是否显示邀请上台的按钮(房主在举手发言房间中可以邀请其他没有上台的用户) - switch viewModel.roomInfo.speechMode { - case .freeToSpeak: - changeInviteStageButtonHidden(isHidden: true) - case .applySpeakAfterTakingSeat: - //房主可以邀请没有上麦的成员上麦 - if viewModel.currentUser.userId == viewModel.roomInfo.ownerId, attendeeModel.userId != viewModel.roomInfo.ownerId, - !attendeeModel.isOnSeat { - changeInviteStageButtonHidden(isHidden: false) - } else { - changeInviteStageButtonHidden(isHidden: true) - } - default: break + guard viewModel.roomInfo.speechMode == .applySpeakAfterTakingSeat else { return } + muteAudioButton.isHidden = !attendeeModel.isOnSeat + muteVideoButton.isHidden = !attendeeModel.isOnSeat + if viewModel.currentUser.userId == viewModel.roomInfo.ownerId { + inviteStageButton.isHidden = attendeeModel.isOnSeat + } else { + inviteStageButton.isHidden = true } } @@ -536,13 +541,6 @@ class UserListCell: UITableViewCell { viewModel.showUserManageViewAction(userId: attendeeModel.userId, userName: attendeeModel.userName) } - //是否显示邀请按钮(如果显示了邀请按钮,麦克风和摄像头按钮不会显示) - private func changeInviteStageButtonHidden(isHidden: Bool) { - inviteStageButton.isHidden = isHidden - muteAudioButton.isHidden = !isHidden - muteVideoButton.isHidden = !isHidden - } - deinit { debugPrint("deinit \(self)") } diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/ScreenCaptureMaskView.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/ScreenCaptureMaskView.swift index f9c1c75e0..d4108062f 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/ScreenCaptureMaskView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/ScreenCaptureMaskView.swift @@ -15,7 +15,7 @@ enum ScreenCaptureMaskViewFrameType { class ScreenCaptureMaskView: UIView { private var dotsTimer: Timer = Timer() - + var viewModel: TUIVideoSeatViewModel? let frameType: ScreenCaptureMaskViewFrameType let contentView: UIView = { @@ -122,18 +122,10 @@ class ScreenCaptureMaskView: UIView { } @objc func stopScreenCaptureAction(sender: UIButton) { - let alertVC = UIAlertController(title: .toastTitleText, - message: .toastMessageText, - preferredStyle: .alert) - let cancelAction = UIAlertAction(title: .toastCancelText, style: .cancel) { _ in - } - let StopAction = UIAlertAction(title: .toastStopText, style: .default) { _ in - EngineEventCenter.shared.notifyEngineEvent(event: .onUserScreenCaptureStopped, param: [:]) - EngineManager.createInstance().stopScreenCapture() - } - alertVC.addAction(cancelAction) - alertVC.addAction(StopAction) - RoomRouter.shared.presentAlert(alertVC) + RoomRouter.presentAlert(title: .toastTitleText, message: .toastMessageText, sureTitle: .toastStopText, declineTitle: .toastCancelText, sureBlock: { [weak self] in + guard let self = self else { return } + self.viewModel?.stopScreenCapture() + }, declineBlock: nil) } @objc func clickMask() { diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatCell.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatCell.swift similarity index 77% rename from iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatCell.swift rename to iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatCell.swift index 97707d537..94bfc0b73 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatCell.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatCell.swift @@ -1,5 +1,5 @@ // -// TUIVideoSeatCell.swift +// VideoSeatCell.swift // TUIVideoSeat // // Created by WesleyLei on 2021/12/16. @@ -10,42 +10,41 @@ import SnapKit import TUIRoomEngine import UIKit -class TUIVideoSeatCell: UICollectionViewCell { +class VideoSeatCell: UICollectionViewCell { var seatItem: VideoSeatItem? - - 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 = 3 - scrollView.minimumZoomScale = 1 - scrollView.delegate = self - return scrollView - }() - + var viewModel: TUIVideoSeatViewModel? + let renderView: UIView = { let view = UIView(frame: .zero) - view.backgroundColor = .clear + view.backgroundColor = UIColor(0x17181F) + view.layer.cornerRadius = 16 + view.layer.masksToBounds = true + view.layer.borderWidth = 2 + view.layer.borderColor = UIColor.clear.cgColor return view }() - - let userInfoView: TUIVideoSeatUserStatusView = { - let view = TUIVideoSeatUserStatusView() + + let backgroundMaskView: UIView = { + let view = UIView(frame: .zero) + view.backgroundColor = UIColor(0x17181F) + view.layer.cornerRadius = 16 + view.layer.masksToBounds = true + view.layer.borderWidth = 2 + view.layer.borderColor = UIColor.clear.cgColor 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() @@ -57,22 +56,20 @@ class TUIVideoSeatCell: UICollectionViewCell { activateConstraints() contentView.backgroundColor = .clear } - + private func constructViewHierarchy() { - scrollRenderView.addSubview(renderView) - contentView.addSubview(scrollRenderView) + contentView.addSubview(renderView) + contentView.addSubview(backgroundMaskView) contentView.addSubview(avatarImageView) contentView.addSubview(userInfoView) } - + private func activateConstraints() { - scrollRenderView.snp.makeConstraints { make in + renderView.snp.makeConstraints { make in make.edges.equalToSuperview().inset(3) } - renderView.snp.makeConstraints { make in - make.center.equalToSuperview() - make.width.equalToSuperview() - make.height.equalToSuperview() + backgroundMaskView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(3) } userInfoView.snp.makeConstraints { make in make.height.equalTo(24) @@ -81,33 +78,29 @@ class TUIVideoSeatCell: UICollectionViewCell { 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 + renderView.layer.borderColor = UIColor.clear.cgColor + backgroundMaskView.layer.borderColor = UIColor.clear.cgColor } - + deinit { NSObject.cancelPreviousPerformRequests(withTarget: self) debugPrint("deinit \(self)") } } -extension TUIVideoSeatCell: UIScrollViewDelegate { - func viewForZooming(in scrollView: UIScrollView) -> UIView? { - return renderView - } -} - // MARK: - Public -extension TUIVideoSeatCell { +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.isHasVideoStream // && (item.isPlaying || item.isSelf) + avatarImageView.isHidden = item.type == .share ? item.isHasVideoStream : item.hasVideoStream + backgroundMaskView.isHidden = item.type == .share ? item.isHasVideoStream : item.hasVideoStream userInfoView.updateUserStatus(item) resetVolumeView() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -119,26 +112,28 @@ extension TUIVideoSeatCell { } } } - + func updateUIVolume(item: VideoSeatItem) { userInfoView.updateUserVolume(hasAudio: item.hasAudioStream, volume: item.audioVolume) if item.audioVolume > 0 && item.hasAudioStream { if item.type != .share { - scrollRenderView.layer.borderColor = UIColor(0xA5FE33).cgColor + renderView.layer.borderColor = UIColor(0xA5FE33).cgColor + backgroundMaskView.layer.borderColor = UIColor(0xA5FE33).cgColor } } else { - scrollRenderView.layer.borderColor = UIColor.clear.cgColor + renderView.layer.borderColor = UIColor.clear.cgColor + backgroundMaskView.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: TUIVideoSeatCell { +class TUIVideoSeatDragCell: VideoSeatCell { typealias DragCellClickBlock = () -> Void private let clickBlock: DragCellClickBlock init(frame: CGRect, clickBlock: @escaping DragCellClickBlock) { @@ -146,11 +141,11 @@ class TUIVideoSeatDragCell: TUIVideoSeatCell { super.init(frame: frame) addGesture() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + func updateSize(size: CGSize) { var frame = self.frame frame.size = size @@ -168,11 +163,11 @@ extension TUIVideoSeatDragCell { 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 } // 移动状态 @@ -196,7 +191,7 @@ extension TUIVideoSeatDragCell { // 重置 panGesture gesture.setTranslation(.zero, in: viewSuperview) } - + private func adsorption(centerPoint: CGPoint) -> CGPoint { guard let viewSuperview = superview else { return centerPoint } let limitMargin = 5.0 diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatLayout.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatLayout.swift similarity index 98% rename from iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatLayout.swift rename to iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatLayout.swift index 9ede94b82..bda4dd444 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatLayout.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatLayout.swift @@ -1,5 +1,5 @@ // -// TUIVideoSeatLayout.swift +// VideoSeatLayout.swift // TUIVideoSeat // // Created by 唐佳宁 on 2023/3/16. @@ -7,11 +7,11 @@ import Foundation -protocol TUIVideoSeatLayoutDelegate: AnyObject { +protocol VideoSeatLayoutDelegate: AnyObject { func updateNumberOfPages(numberOfPages: NSInteger) } -class TUIVideoSeatLayout: UICollectionViewFlowLayout { +class VideoSeatLayout: UICollectionViewFlowLayout { private var prePageCount: NSInteger = 1 private var collectionViewHeight: CGFloat { @@ -75,7 +75,7 @@ class TUIVideoSeatLayout: UICollectionViewFlowLayout { return CGSize(width: CGFloat(prePageCount) * collectionViewWidth, height: collectionViewHeight) } - weak var delegate: TUIVideoSeatLayoutDelegate? + weak var delegate: VideoSeatLayoutDelegate? // Miniscreen布局信息 func getMiniscreenFrame(item: VideoSeatItem?) -> CGRect { @@ -91,7 +91,7 @@ class TUIVideoSeatLayout: UICollectionViewFlowLayout { // MARK: - layout -extension TUIVideoSeatLayout { +extension VideoSeatLayout { // 计算cell的位置和大小,并进行存储 private func calculateEachCellFrame() { guard let collectionViewWidth: CGFloat = collectionView?.bounds.width else { return } diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatUserStatusView.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatUserStatusView.swift similarity index 94% rename from iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatUserStatusView.swift rename to iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatUserStatusView.swift index 731c966f6..b2946d33b 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatUserStatusView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatUserStatusView.swift @@ -1,5 +1,5 @@ // -// TUIVideoSeatUserStatusView.swift +// VideoSeatUserStatusView.swift // TUIVideoSeat // // Created by jack on 2023/3/6. @@ -7,7 +7,7 @@ import Foundation -class TUIVideoSeatUserStatusView: UIView { +class VideoSeatUserStatusView: UIView { private var isOwner: Bool = false private var isViewReady: Bool = false override func didMoveToWindow() { @@ -77,14 +77,14 @@ class TUIVideoSeatUserStatusView: UIView { // MARK: - Public -extension TUIVideoSeatUserStatusView { +extension VideoSeatUserStatusView { func updateUserStatus(_ item: VideoSeatItem) { if !item.userName.isEmpty { userNameLabel.text = item.userName } else { userNameLabel.text = item.userId } - isOwner = item.userRole == .roomOwner + isOwner = item.userId == EngineManager.createInstance().store.roomInfo.ownerId homeOwnerImageView.isHidden = !isOwner updateOwnerImageConstraints() updateUserVolume(hasAudio: item.hasAudioStream, volume: item.audioVolume) diff --git a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatView.swift b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatView.swift similarity index 61% rename from iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatView.swift rename to iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatView.swift index 29a5b7faf..f73df0e16 100644 --- a/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/TUIVideoSeatView.swift +++ b/iOS/TUIRoomKit/Source/View/Page/Widget/VideoSeat/VideoSeatView.swift @@ -9,16 +9,16 @@ import TUIRoomEngine import UIKit #if TXLiteAVSDK_TRTC - import TXLiteAVSDK_TRTC +import TXLiteAVSDK_TRTC #elseif TXLiteAVSDK_Professional - import TXLiteAVSDK_Professional +import TXLiteAVSDK_Professional #endif class TUIVideoSeatView: UIView { - private let CellID_Normal = "TUIVideoSeatCell_Normal" + private let CellID_Normal = "VideoSeatCell_Normal" private let viewModel: TUIVideoSeatViewModel private var isViewReady: Bool = false - + private var pageControl: UIPageControl = { let control = UIPageControl() control.currentPage = 0 @@ -27,14 +27,14 @@ class TUIVideoSeatView: UIView { control.isUserInteractionEnabled = false return control }() - - init(frame: CGRect, roomEngine: TUIRoomEngine, roomId: String) { - viewModel = TUIVideoSeatViewModel(roomEngine: roomEngine, roomId: roomId) - super.init(frame: frame) + + init() { + viewModel = TUIVideoSeatViewModel() + super.init(frame: .zero) viewModel.viewResponder = self isUserInteractionEnabled = true } - + override func didMoveToWindow() { super.didMoveToWindow() guard !isViewReady else { return } @@ -43,7 +43,7 @@ class TUIVideoSeatView: UIView { bindInteraction() isViewReady = true } - + override func layoutSubviews() { super.layoutSubviews() if let item = moveMiniscreen.seatItem,!moveMiniscreen.isHidden { @@ -56,21 +56,21 @@ class TUIVideoSeatView: UIView { 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: TUIVideoSeatLayout = { - let layout = TUIVideoSeatLayout(viewModel: viewModel) + + 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(TUIVideoSeatCell.self, forCellWithReuseIdentifier: CellID_Normal) + videoSeatLayout) + collection.register(VideoSeatCell.self, forCellWithReuseIdentifier: CellID_Normal) collection.isPagingEnabled = true collection.showsVerticalScrollIndicator = false collection.showsHorizontalScrollIndicator = false @@ -91,7 +91,7 @@ class TUIVideoSeatView: UIView { collection.delegate = self return collection }() - + lazy var moveMiniscreen: TUIVideoSeatDragCell = { let cell = TUIVideoSeatDragCell(frame: videoSeatLayout.getMiniscreenFrame(item: nil)) { [weak self] in guard let self = self else { return } @@ -104,18 +104,29 @@ class TUIVideoSeatView: UIView { lazy var screenCaptureMaskView: ScreenCaptureMaskView = { let view = ScreenCaptureMaskView(frameType: .fullScreen) + view.viewModel = self.viewModel 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() } @@ -134,30 +145,30 @@ class TUIVideoSeatView: UIView { screenCaptureMaskView.isHidden = !EngineManager.createInstance().store.currentUser.hasScreenStream addGesture() } - + private func addGesture() { let tap = UITapGestureRecognizer(target: self, action: #selector(clickVideoSeat)) addGestureRecognizer(tap) } - + @objc private func clickVideoSeat() { viewModel.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 { viewModel.startPlayVideo(item: seatItem, renderView: moveMiniscreen.renderView) } else { - viewModel.startPlayVideo(item: seatItem, renderView: getSeatVideoRenderView(seatItem)) + viewModel.startPlayVideo(item: seatItem, renderView: getVideoVisibleCell(seatItem)?.renderView) } } } - + deinit { debugPrint("deinit \(self)") } @@ -166,94 +177,78 @@ class TUIVideoSeatView: UIView { // MARK: - TUIVideoSeatViewResponder extension TUIVideoSeatView: TUIVideoSeatViewResponder { - private func freshCollectionView(block: () -> Void) { - CATransaction.begin() - CATransaction.setDisableActions(true) - CATransaction.setCompletionBlock { [weak self] in - guard let self = self else { return } - self.viewModel.clearSubscribeVideoStream(items: self.getNoLoadHasVideoItems()) - } - block() - CATransaction.commit() - } - func reloadData() { - freshCollectionView { - self.attendeeCollectionView.reloadData() - } + self.attendeeCollectionView.reloadData() } - + func insertItems(at indexPaths: [IndexPath]) { - freshCollectionView { - self.attendeeCollectionView.performBatchUpdates { - //如果当前的cell数量已经和数据源相同,不再进行插入操作而是直接reloadData - if self.attendeeCollectionView.numberOfItems(inSection: 0) == viewModel.listSeatItem.count { - attendeeCollectionView.reloadData() - } else { - self.attendeeCollectionView.insertItems(at: indexPaths) - } + self.attendeeCollectionView.performBatchUpdates { [weak self] in + guard let self = self else { return } + //如果当前的cell数量已经和数据源相同,不再进行插入操作而是直接reloadData + if self.attendeeCollectionView.numberOfItems(inSection: 0) == self.viewModel.listSeatItem.count { + self.attendeeCollectionView.reloadData() + } else { + self.attendeeCollectionView.insertItems(at: indexPaths) } } } func deleteItems(at indexPaths: [IndexPath]) { - freshCollectionView { - self.attendeeCollectionView.performBatchUpdates { - //如果当前cell的数量已经和数据源相同,不再进行删除操作,而是直接reloadData - if attendeeCollectionView.numberOfItems(inSection: 0) == viewModel.listSeatItem.count { - attendeeCollectionView.reloadData() - } else { - 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 } // indexPath越界,不执行删除操作 - resultArray.append(indexPath) - } - self.attendeeCollectionView.deleteItems(at: resultArray) + self.attendeeCollectionView.performBatchUpdates { [weak self] in + guard let self = self else { return } + //如果当前cell的数量已经和数据源相同,不再进行删除操作,而是直接reloadData + if self.attendeeCollectionView.numberOfItems(inSection: 0) == self.viewModel.listSeatItem.count { + self.attendeeCollectionView.reloadData() + } else { + 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 } // indexPath越界,不执行删除操作 + resultArray.append(indexPath) } + self.attendeeCollectionView.deleteItems(at: resultArray) } } } - - func reloadItems(at indexPaths: [IndexPath]) { - freshCollectionView { - self.attendeeCollectionView.performBatchUpdates { - self.attendeeCollectionView.reloadItems(at: indexPaths) - } - } + + func deleteVideoSeatCell(item: VideoSeatItem) { + guard let cell = getVideoVisibleCell(item) else { return } + cell.removeFromSuperview() } func updateSeatItem(_ item: VideoSeatItem) { if let seatItem = moveMiniscreen.seatItem, seatItem.userId == item.userId { moveMiniscreen.updateUI(item: seatItem) } - guard let cell = item.boundCell else { return } - if item == cell.seatItem { - cell.updateUI(item: item) - if item.hasVideoStream { - viewModel.startPlayVideo(item: item, renderView: cell.renderView) - } + guard let cell = getVideoVisibleCell(item) else { return } + cell.updateUI(item: item) + if item.hasVideoStream { + viewModel.startPlayVideo(item: item, renderView: cell.renderView) + } else { + viewModel.stopPlayVideo(item: item) } } - + func updateSeatVolume(_ item: VideoSeatItem) { - guard let cell = item.boundCell else { return } - if cell.seatItem == item { - cell.updateUIVolume(item: item) - } + guard let cell = getVideoVisibleCell(item) else { return } + cell.updateUIVolume(item: item) } - - func getSeatVideoRenderView(_ item: VideoSeatItem) -> UIView? { - guard let cell = item.boundCell else { return nil } - if cell.seatItem == item { - return cell.renderView - } - return nil + + 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 @@ -262,27 +257,19 @@ extension TUIVideoSeatView: TUIVideoSeatViewResponder { if attendeeCollectionView.contentOffset.x > 0 { return } - - if let lastSeatItem = moveMiniscreen.seatItem, lastSeatItem.isHasVideoStream { - if let renderView = getSeatVideoRenderView(lastSeatItem) { - viewModel.startPlayVideo(item: lastSeatItem, renderView: renderView) - } else { - viewModel.unboundCell(item: lastSeatItem) - } - } - moveMiniscreen.updateSize(size: videoSeatLayout.getMiniscreenFrame(item: item).size) moveMiniscreen.isHidden = false + bringSubviewToFront(moveMiniscreen) moveMiniscreen.updateUI(item: item) if item.isHasVideoStream { viewModel.startPlayVideo(item: item, renderView: moveMiniscreen.renderView) } } - + func updateMiniscreenVolume(_ item: VideoSeatItem) { moveMiniscreen.updateUIVolume(item: item) } - + func getMoveMiniscreen() -> TUIVideoSeatDragCell { return moveMiniscreen } @@ -293,29 +280,6 @@ extension TUIVideoSeatView: TUIVideoSeatViewResponder { screenCaptureMaskView.superview?.bringSubviewToFront(screenCaptureMaskView) } } - - private func getNoLoadHasVideoItems() -> [VideoSeatItem] { - var hasVideoStreamItems = viewModel.listSeatItem.filter({ $0.isHasVideoStream }) - let visibleCells = Array(attendeeCollectionView.visibleCells) - for cell in visibleCells { - if let seatCell = cell as? TUIVideoSeatCell, - let seatItem = seatCell.seatItem, - let seatItemIndex = hasVideoStreamItems.firstIndex(where: { $0 == seatItem }) { - hasVideoStreamItems.remove(at: seatItemIndex) - } - } - if let seatItem = moveMiniscreen.seatItem, - let seatItemIndex = hasVideoStreamItems.firstIndex(where: { - if $0.userId == seatItem.userId && $0.type != .share { - return true - } else { - return false - } - }) { - hasVideoStreamItems.remove(at: seatItemIndex) - } - return hasVideoStreamItems - } } // MARK: - UICollectionViewDelegateFlowLayout @@ -323,16 +287,18 @@ extension TUIVideoSeatView: TUIVideoSeatViewResponder { 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? TUIVideoSeatCell else { return } - if let lastSeatItem = seatCell.seatItem, lastSeatItem != seatItem, lastSeatItem.boundCell == cell { - lastSeatItem.boundCell = nil - viewModel.stopPlayVideo(item: lastSeatItem) - } - seatItem.boundCell = seatCell - seatCell.updateUI(item: seatItem) + guard let seatCell = cell as? VideoSeatCell else { return } if seatItem.isHasVideoStream { viewModel.startPlayVideo(item: seatItem, renderView: seatCell.renderView) + } else { + viewModel.stopPlayVideo(item: seatItem) } + seatCell.updateUI(item: seatItem) + } + + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + guard let seatItem = viewModel.listSeatItem[safe: indexPath.item] else { return } + viewModel.stopPlayVideo(item: seatItem) } } @@ -345,10 +311,9 @@ extension TUIVideoSeatView: UIScrollViewDelegate { } else { attendeeCollectionView.addSubview(moveMiniscreen) } - viewModel.clearSubscribeVideoStream(items: getNoLoadHasVideoItems()) updatePageControl() } - + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { attendeeCollectionView.addSubview(moveMiniscreen) } @@ -360,38 +325,27 @@ extension TUIVideoSeatView: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } - + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + debugPrint("**********,collectionView,numberOfItemsInSection:\(viewModel.listSeatItem.count)") return viewModel.listSeatItem.count } - + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell( withReuseIdentifier: CellID_Normal, - for: indexPath) as! TUIVideoSeatCell + for: indexPath) as! VideoSeatCell if indexPath.item >= viewModel.listSeatItem.count { return cell } - - // 解绑cell和item的绑定 - let seatItem = viewModel.listSeatItem[indexPath.item] - if let lastSeatItem = cell.seatItem, lastSeatItem != seatItem, lastSeatItem.boundCell == cell { - lastSeatItem.boundCell = nil - viewModel.stopPlayVideo(item: lastSeatItem) - } - - seatItem.boundCell = cell - cell.updateUI(item: seatItem) - if seatItem.isHasVideoStream { - viewModel.startPlayVideo(item: seatItem, renderView: cell.renderView) - } + cell.viewModel = self.viewModel return cell } } // MARK: - UICollectionViewDataSource -extension TUIVideoSeatView: TUIVideoSeatLayoutDelegate { +extension TUIVideoSeatView: VideoSeatLayoutDelegate { func updateNumberOfPages(numberOfPages: NSInteger) { pageControl.numberOfPages = numberOfPages } diff --git a/iOS/TUIRoomKit/Source/ViewModel/BottomViewModel.swift b/iOS/TUIRoomKit/Source/ViewModel/BottomViewModel.swift index ed97bef18..94dd9e3ab 100644 --- a/iOS/TUIRoomKit/Source/ViewModel/BottomViewModel.swift +++ b/iOS/TUIRoomKit/Source/ViewModel/BottomViewModel.swift @@ -14,6 +14,7 @@ protocol BottomViewModelResponder: AnyObject { func updateStackView(item: ButtonItemData, index: Int) func makeToast(text: String) func updataBottomView(isUp:Bool) + func showStopShareScreenAlert(sureBlock: (()->())?) } class BottomViewModel: NSObject { @@ -346,20 +347,10 @@ class BottomViewModel: NSObject { BroadcastLauncher.launch() } } else { - let alertVC = UIAlertController(title: .toastTitleText, - message: .toastMessageText, - preferredStyle: .alert) - let cancelAction = UIAlertAction(title: .toastCancelText, style: .cancel) { _ in - } - let StopAction = UIAlertAction(title: .toastStopText, style: .default) { [weak self] _ in - guard let self = self else { - return - } + viewResponder?.showStopShareScreenAlert(sureBlock: { [weak self] in + guard let self = self else { return } self.engineManager.stopScreenCapture() - } - alertVC.addAction(cancelAction) - alertVC.addAction(StopAction) - RoomRouter.shared.presentAlert(alertVC) + }) } } else { viewResponder?.makeToast(text: .versionLowToastText) @@ -640,18 +631,6 @@ private extension String { static var dropText: String { localized("TUIRoom.drop") } - static var toastTitleText: String { - localized("TUIRoom.toast.shareScreen.title") - } - static var toastMessageText: String { - localized("TUIRoom.toast.shareScreen.message") - } - static var toastCancelText: String { - localized("TUIRoom.toast.shareScreen.cancel") - } - static var toastStopText: String { - localized("TUIRoom.toast.shareScreen.stop") - } static var putHandsDownText: String { localized("TUIRoom.put.hands.down") } diff --git a/iOS/TUIRoomKit/Source/ViewModel/ExitRoomViewModel.swift b/iOS/TUIRoomKit/Source/ViewModel/ExitRoomViewModel.swift index 868faea1a..50261cf45 100644 --- a/iOS/TUIRoomKit/Source/ViewModel/ExitRoomViewModel.swift +++ b/iOS/TUIRoomKit/Source/ViewModel/ExitRoomViewModel.swift @@ -8,11 +8,16 @@ import Foundation import TUIRoomEngine +protocol ExitRoomViewModelResponder: AnyObject { + func makeToast(message: String) +} + class ExitRoomViewModel { var engineManager: EngineManager var currentUser: UserEntity var isRoomOwner: Bool var isOnlyOneUserInRoom: Bool + weak var viewResponder: ExitRoomViewModelResponder? init() { engineManager = EngineManager.createInstance() @@ -21,7 +26,7 @@ class ExitRoomViewModel { isOnlyOneUserInRoom = engineManager.store.attendeeList.count == 1 } - func isShowLeaveRoomButton() -> Bool { + func isShownLeaveRoomButton() -> Bool { if currentUser.userId == engineManager.store.roomInfo.ownerId { return engineManager.store.attendeeList.count > 1 } else { @@ -29,6 +34,10 @@ class ExitRoomViewModel { } } + func isShownExitRoomButton() -> Bool { + return currentUser.userId == engineManager.store.roomInfo.ownerId + } + func leaveRoomAction() { if isRoomOwner && !isOnlyOneUserInRoom { RoomRouter.shared.presentPopUpViewController(viewType: .transferMasterViewType, height: 720.scale375Height()) @@ -39,12 +48,22 @@ class ExitRoomViewModel { func exitRoom() { if isRoomOwner { - engineManager.destroyRoom(onSuccess: nil, onError: nil) + engineManager.destroyRoom { + RoomRouter.shared.dismissAllRoomPopupViewController() + RoomRouter.shared.popToRoomEntranceViewController() + } onError: { [weak self] code, message in + guard let self = self else { return } + self.viewResponder?.makeToast(message: message) + } } else { - engineManager.exitRoom(onSuccess: nil, onError: nil) + engineManager.exitRoom { + RoomRouter.shared.dismissAllRoomPopupViewController() + RoomRouter.shared.popToRoomEntranceViewController() + } onError: { [weak self] code, message in + guard let self = self else { return } + self.viewResponder?.makeToast(message: message) + } } - RoomRouter.shared.dismissAllRoomPopupViewController() - RoomRouter.shared.popToRoomEntranceViewController() } deinit { diff --git a/iOS/TUIRoomKit/Source/ViewModel/RoomMainViewModel.swift b/iOS/TUIRoomKit/Source/ViewModel/RoomMainViewModel.swift index bd2198951..d913812ee 100644 --- a/iOS/TUIRoomKit/Source/ViewModel/RoomMainViewModel.swift +++ b/iOS/TUIRoomKit/Source/ViewModel/RoomMainViewModel.swift @@ -21,7 +21,7 @@ protocol RoomMainViewResponder: AnyObject { func setToolBarDelayHidden(isDelay: Bool) func updateMuteAudioButton(isSelected: Bool) func showExitRoomView() - func showAlert(title: String?, message: String?, sureBlock: (()->())?, declineBlock: (() -> ())?) + func showAlert(title: String?, message: String?, sureTitle:String?, declineTitle: String?, sureBlock: (() -> ())?, declineBlock: (() -> ())?) } class RoomMainViewModel: NSObject { @@ -116,7 +116,7 @@ extension RoomMainViewModel: RoomEngineEventResponder { func onEngineEvent(name: EngineEventCenter.RoomEngineEvent, param: [String : Any]?) { if name == .onRoomDismissed { engineManager.destroyEngineManager() - viewResponder?.showAlert(title: .destroyAlertText, message: nil, sureBlock: { [weak self] in + viewResponder?.showAlert(title: .destroyAlertText, message: nil, sureTitle: .alertOkText, declineTitle: nil, sureBlock: { [weak self] in guard let self = self else { return } self.roomRouter.dismissAllRoomPopupViewController() self.roomRouter.popToRoomEntranceViewController() @@ -125,7 +125,7 @@ extension RoomMainViewModel: RoomEngineEventResponder { if name == .onKickedOutOfRoom { engineManager.destroyEngineManager() - viewResponder?.showAlert(title: .kickOffTitleText, message: nil, sureBlock: { [weak self] in + viewResponder?.showAlert(title: .kickOffTitleText, message: nil, sureTitle: .alertOkText, declineTitle: nil , sureBlock: { [weak self] in guard let self = self else { return } self.roomRouter.dismissAllRoomPopupViewController() self.roomRouter.popToRoomEntranceViewController() @@ -137,7 +137,7 @@ extension RoomMainViewModel: RoomEngineEventResponder { switch request.requestAction { case .openRemoteCamera: guard !isShownOpenCameraInviteAlert else { return } - viewResponder?.showAlert(title: .inviteTurnOnVideoText, message: nil, sureBlock: { [weak self] in + viewResponder?.showAlert(title: .inviteTurnOnVideoText, message: nil, sureTitle: .agreeText, declineTitle: .declineText, sureBlock: { [weak self] in guard let self = self else { return } self.isShownOpenCameraInviteAlert = false // FIXME: - 打开摄像头前需要先设置一个view @@ -160,7 +160,7 @@ extension RoomMainViewModel: RoomEngineEventResponder { isShownOpenCameraInviteAlert = true case .openRemoteMicrophone: guard !isShownOpenMicrophoneInviteAlert else { return } - viewResponder?.showAlert(title: .inviteTurnOnAudioText, message: nil, sureBlock: { [weak self] in + viewResponder?.showAlert(title: .inviteTurnOnAudioText, message: nil, sureTitle: .agreeText, declineTitle: .declineText, sureBlock: { [weak self] in guard let self = self else { return } self.isShownOpenMicrophoneInviteAlert = false if RoomCommon.checkAuthorMicStatusIsDenied() { @@ -185,7 +185,7 @@ extension RoomMainViewModel: RoomEngineEventResponder { switch roomInfo.speechMode { case .applySpeakAfterTakingSeat: guard !isShownTakeSeatInviteAlert else { return } - viewResponder?.showAlert(title: .inviteSpeakOnStageTitle, message: .inviteSpeakOnStageMessage, sureBlock: { [weak self] in + viewResponder?.showAlert(title: .inviteSpeakOnStageTitle, message: .inviteSpeakOnStageMessage, sureTitle: .agreeSeatText, declineTitle: .declineText, sureBlock: { [weak self] in guard let self = self else { return } self.isShownTakeSeatInviteAlert = false self.respondUserOnSeat(isAgree: true, requestId: request.requestId) @@ -219,8 +219,7 @@ extension RoomMainViewModel: RoomMainViewFactory { } func makeVideoSeatView() -> UIView { - let videoSeatView = TUIVideoSeatView(frame: UIScreen.main.bounds, roomEngine: engineManager.roomEngine, - roomId: roomInfo.roomId) + let videoSeatView = TUIVideoSeatView() videoSeatView.backgroundColor = UIColor(0x0F1014) return videoSeatView } @@ -274,7 +273,7 @@ extension RoomMainViewModel: RoomKitUIEventResponder { case .TUIRoomKitService_CurrentUserRoleChanged: guard let userRole = info?["userRole"] as? TUIRole else { return } guard userRole == .roomOwner else { return } - viewResponder?.showAlert(title: .haveBecomeMasterText, message: nil, sureBlock: nil, declineBlock: nil) + viewResponder?.showAlert(title: .haveBecomeMasterText, message: nil,sureTitle: .alertOkText, declineTitle: nil, sureBlock: nil, declineBlock: nil) case .TUIRoomKitService_CurrentUserMuteMessage: guard let isMute = info?["isMute"] as? Bool else { return } viewResponder?.makeToast(text: isMute ? .messageTurnedOffText : .messageTurnedOnText) @@ -329,4 +328,16 @@ private extension String { static var kickedOffLineText: String { localized("TUIRoom.kicked.off.line") } + static var alertOkText: String { + localized("TUIRoom.ok") + } + static var declineText: String { + localized("TUIRoom.decline") + } + static var agreeText: String { + localized("TUIRoom.agree") + } + static var agreeSeatText: String { + localized("TUIRoom.agree.seat") + } } diff --git a/iOS/TUIRoomKit/Source/ViewModel/TUIVideoSeatViewModel.swift b/iOS/TUIRoomKit/Source/ViewModel/TUIVideoSeatViewModel.swift index 4d90485e4..0c2b85570 100644 --- a/iOS/TUIRoomKit/Source/ViewModel/TUIVideoSeatViewModel.swift +++ b/iOS/TUIRoomKit/Source/ViewModel/TUIVideoSeatViewModel.swift @@ -9,23 +9,22 @@ import Foundation import TUIRoomEngine #if TXLiteAVSDK_TRTC - import TXLiteAVSDK_TRTC +import TXLiteAVSDK_TRTC #elseif TXLiteAVSDK_Professional - import TXLiteAVSDK_Professional +import TXLiteAVSDK_Professional #endif protocol TUIVideoSeatViewResponder: AnyObject { func reloadData() func insertItems(at indexPaths: [IndexPath]) func deleteItems(at indexPaths: [IndexPath]) - func reloadItems(at indexPaths: [IndexPath]) - - func getSeatVideoRenderView(_ item: VideoSeatItem) -> UIView? + + func getVideoVisibleCell(_ item: VideoSeatItem) -> VideoSeatCell? func getMoveMiniscreen() -> TUIVideoSeatDragCell - + func updateMiniscreen(_ item: VideoSeatItem?) func updateMiniscreenVolume(_ item: VideoSeatItem) - + func updateSeatItem(_ item: VideoSeatItem) func updateSeatVolume(_ item: VideoSeatItem) @@ -50,9 +49,7 @@ class TUIVideoSeatViewModel: NSObject { private var speakerItem: VideoSeatItem? // 小画面item private var smallItem: VideoSeatItem? - - private var userNoExistMap: [String: TUIUserInfo] = [:] - + private var isSwitchPosition: Bool = false private var speakerUpdateTimer: Int = 0 //演讲者更新的时间戳 @@ -64,40 +61,77 @@ class TUIVideoSeatViewModel: NSObject { return .cameraStream } } - + var listSeatItem: [VideoSeatItem] = [] - + private var isHasVideoStream: Bool { return videoSeatItems.firstIndex(where: { $0.isHasVideoStream }) != nil } - + private var isHasScreenStream: Bool { return videoSeatItems.firstIndex(where: { $0.hasScreenStream }) != nil } - - private var isNeedReloadStream: Bool { - return videoSeatItems.firstIndex(where: { $0.boundCell != nil }) == nil - } - + weak var viewResponder: TUIVideoSeatViewResponder? var videoSeatViewType: TUIVideoSeatViewType = .unknown - var roomInfo: TUIRoomInfo + var engineManager: EngineManager { + EngineManager.createInstance() + } + var store: RoomStore { + engineManager.store + } + var roomInfo: TUIRoomInfo { + store.roomInfo + } var currentUserId: String { - return TUIRoomEngine.getSelfInfo().userId + store.currentUser.userId } - - private weak var roomEngine: TUIRoomEngine? - init(roomEngine: TUIRoomEngine, roomId: String) { - roomInfo = TUIRoomInfo() - roomInfo.roomId = roomId + + override init() { super.init() - self.roomEngine = roomEngine - initRoomInfo() - roomEngine.addObserver(self) + initVideoSeatItems() + subscribeUIEvent() } - + + private func initVideoSeatItems() { + videoSeatItems = [] + let videoItems = store.roomInfo.speechMode == .applySpeakAfterTakingSeat ? store.seatList : store.attendeeList + guard videoItems.count > 0 else { return } + videoItems.forEach { userInfo in + let userItem = VideoSeatItem(userInfo: userInfo) + videoSeatItems.append(userItem) + } + 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) + } + + 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) + } + deinit { - roomEngine?.removeObserver(self) + unsubscribeUIEvent() + debugPrint("deinit:\(self)") } } @@ -107,14 +141,15 @@ extension TUIVideoSeatViewModel { func isHomeOwner(_ userId: String) -> Bool { return roomInfo.ownerId == userId } - + func switchPosition() { if videoSeatViewType == .largeSmallWindowType { isSwitchPosition = !isSwitchPosition reloadSeatItems() + viewResponder?.reloadData() } } - + func updateSpeakerPlayVideoState(currentPageIndex: Int) { // currentPageIndex : 0,1,2,3 if videoSeatViewType != .speechType { @@ -123,64 +158,46 @@ extension TUIVideoSeatViewModel { if currentPageIndex == 0 { viewResponder?.updateMiniscreen(speakerItem) } else if let item = videoSeatItems.first(where: { $0.userId == speakerItem?.userId }), - let renderView = viewResponder?.getSeatVideoRenderView(item) { + let renderView = viewResponder?.getVideoVisibleCell(item)?.renderView { startPlayVideo(item: item, renderView: renderView) } } - + func startPlayVideo(item: VideoSeatItem, renderView: UIView?) { guard let renderView = renderView else { return } if item.userId == currentUserId { - roomEngine?.setLocalVideoView(streamType: item.streamType, view: renderView) + engineManager.setLocalVideoView(streamType: item.streamType, view: renderView) } else { item.updateStreamType(streamType: itemStreamType) - if item.isPlaying { - roomEngine?.setRemoteVideoView(userId: item.userId, streamType: item.streamType, view: renderView) - return - } - roomEngine?.setRemoteVideoView(userId: item.userId, streamType: item.streamType, view: renderView) - roomEngine?.startPlayRemoteVideo(userId: item.userId, streamType: item.streamType, onPlaying: { [weak self] _ in + engineManager.setRemoteVideoView(userId: item.userId, streamType: item.streamType, view: renderView) + engineManager.startPlayRemoteVideo(userId: item.userId, streamType: item.streamType, onSuccess: { [weak self] in guard let self = self else { return } - item.isPlaying = true - if let seatCell = item.boundCell, item == seatCell.seatItem { - seatCell.updateUI(item: item) - } if item == self.viewResponder?.getMoveMiniscreen().seatItem { self.viewResponder?.getMoveMiniscreen().updateUI(item: item) } - }, onLoading: { _ in - }, onError: { _, _, _ in }) } + guard let seatCell = viewResponder?.getVideoVisibleCell(item) else { return } + seatCell.updateUI(item: item) } - + func stopPlayVideo(item: VideoSeatItem) { unboundCell(item: item) if item.userId != currentUserId { - item.isPlaying = false - roomEngine?.stopPlayRemoteVideo(userId: item.userId, streamType: item.streamType) + engineManager.stopPlayRemoteVideo(userId: item.userId, streamType: item.streamType) } + guard let seatCell = viewResponder?.getVideoVisibleCell(item) else { return } + seatCell.updateUI(item: item) } - - func unboundCell(item: VideoSeatItem) { + + private func unboundCell(item: VideoSeatItem) { if item.userId == currentUserId { - roomEngine?.setLocalVideoView(streamType: item.streamType, view: nil) + engineManager.setLocalVideoView(streamType: item.streamType, view: nil) } else { if item.streamType != .screenStream { - roomEngine?.setRemoteVideoView(userId: item.userId, streamType: .cameraStreamLow, view: nil) - } - roomEngine?.setRemoteVideoView(userId: item.userId, streamType: item.streamType, view: nil) - } - } - - func clearSubscribeVideoStream(items: [VideoSeatItem]) { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - for item in items { - if self.viewResponder?.getSeatVideoRenderView(item) == nil { - self.stopPlayVideo(item: item) - } + engineManager.setRemoteVideoView(userId: item.userId, streamType: .cameraStreamLow, view: nil) } + engineManager.setRemoteVideoView(userId: item.userId, streamType: item.streamType, view: nil) } } @@ -189,133 +206,86 @@ extension TUIVideoSeatViewModel { guard RoomRouter.shared.hasChatWindow() else { return } EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_HiddenChatWindow, param: [:]) } + + func stopScreenCapture() { + EngineEventCenter.shared.notifyEngineEvent(event: .onUserScreenCaptureStopped, param: [:]) + engineManager.stopScreenCapture() + } } // MARK: - Private extension TUIVideoSeatViewModel { - private func asyncUserInfo(_ seatItem: VideoSeatItem) { - roomEngine?.getUserInfo(seatItem.userId, onSuccess: { [weak self] userInfo in - guard let self = self, let userInfo = userInfo else { return } - seatItem.updateUserInfo(userInfo) - self.viewResponder?.updateSeatItem(seatItem) - self.reloadSeatItems() - }, onError: { _, msg in - debugPrint("asyncUserInfo error: \(msg)") - }) - } - - private func initRoomInfo() { - roomEngine?.fetchRoomInfo { [weak self] roomInfo in - guard let self = self, let roomInfo = roomInfo else { return } - self.roomInfo = roomInfo - switch self.roomInfo.speechMode { - case .freeToSpeak: - let localSeatList: [VideoSeatItem] = [] - self.initUserList(nextSequence: 0, localSeatList: localSeatList) - case .applySpeakAfterTakingSeat: - self.initSeatList() - default: break - } - } onError: { code, message in - debugPrint("getRoomInfo:code:\(code),message:\(message)") - } - } - - private func initSeatList() { - roomEngine?.getSeatList { [weak self] seatList in - guard let self = self else { return } - var localSeatList = [VideoSeatItem]() - for seatInfo in seatList { - let seatItem = VideoSeatItem(userId: seatInfo.userId ?? "") - self.asyncUserInfo(seatItem) - if seatItem.userId == self.roomInfo.ownerId { - seatItem.userRole = .roomOwner - } - if let userInfo = self.userNoExistMap[seatItem.userId] { - seatItem.hasVideoStream = userInfo.hasVideoStream - seatItem.hasScreenStream = userInfo.hasScreenStream - seatItem.hasAudioStream = userInfo.hasAudioStream - } - localSeatList.append(seatItem) - } - self.videoSeatItems = localSeatList - self.reloadSeatItems() - } onError: { code, message in - debugPrint("getSeatList:code:\(code),message:\(message)") - } - } - - private func initUserList(nextSequence: Int, localSeatList: [VideoSeatItem]) { - roomEngine?.getUserList(nextSequence: nextSequence) { [weak self] list, nextSequence in - guard let self = self else { return } - var localSeatList = localSeatList - list.forEach { userInfo in - let userItem = VideoSeatItem(userInfo: userInfo) - localSeatList.append(userItem) - } - if nextSequence != 0 { - self.initUserList(nextSequence: nextSequence, localSeatList: localSeatList) - } - self.videoSeatItems = localSeatList - self.reloadSeatItems() - } onError: { code, message in - debugPrint("getUserList:code:\(code),message:\(message)") - } - } - - private func addSeatInfo(_ seatInfo: TUISeatInfo) { - if let userId = seatInfo.userId, let _ = getSeatItem(userId) { - return - } - let seatItem = VideoSeatItem(userId: seatInfo.userId ?? "") - asyncUserInfo(seatItem) + private func addUserInfo(_ userId: String) { + guard let userInfo = store.attendeeList.first(where: { $0.userId == userId }) else { return } + let seatItem = VideoSeatItem(userInfo: userInfo) videoSeatItems.append(seatItem) reloadSeatItems() } - - private func addUserInfo(_ userInfo: TUIUserInfo) { - if let userItem = getSeatItem(userInfo.userId) { - userItem.updateUserInfo(userInfo) - viewResponder?.updateSeatItem(userItem) - } else { - let seatItem = VideoSeatItem(userInfo: userInfo) - videoSeatItems.append(seatItem) - reloadSeatItems() - } - } - + 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) } - guard let seatItemIndex = videoSeatItems.firstIndex(where: { $0.userId == userId }) else { return } - let seatItem = videoSeatItems.remove(at: seatItemIndex) - stopPlayVideo(item: seatItem) - reloadSeatItems() + if let seatItem = videoSeatItems.first(where: { $0.userId == userId }) { + stopPlayVideo(item: seatItem) + } + videoSeatItems.removeAll(where: { $0.userId == userId }) + if let index = listSeatItem.firstIndex(where: { $0.userId == userId && $0.type != .share }), + let item = listSeatItem.first(where: { $0.userId == userId && $0.type != .share }) { + listSeatItem.remove(at: index) + if ((viewResponder?.getVideoVisibleCell(item)) != nil) { + viewResponder?.reloadData() + } else { + viewResponder?.deleteItems(at: [IndexPath(item: index, section: 0)]) + } + } + let type = videoSeatViewType + self.refreshListSeatItem() + if type != videoSeatViewType { + viewResponder?.reloadData() + } + self.resetMiniscreen() } - + private func getSeatItem(_ userId: String) -> VideoSeatItem? { return videoSeatItems.first(where: { $0.userId == userId }) } - + private func sortSeatItems() { + guard checkNeededSort() else { return } // 自己在第二位 if let currentItemIndex = videoSeatItems.firstIndex(where: { $0.userId == self.currentUserId }) { let currentItem = videoSeatItems.remove(at: currentItemIndex) videoSeatItems.insert(currentItem, at: 0) } // 房主永远在第一位 - if let roomOwnerItemIndex = videoSeatItems.firstIndex(where: { $0.userRole == .roomOwner }) { + if let roomOwnerItemIndex = videoSeatItems.firstIndex(where: { $0.userId == roomInfo.ownerId }) { let roomOwnerItem = videoSeatItems.remove(at: roomOwnerItemIndex) videoSeatItems.insert(roomOwnerItem, at: 0) } + + viewResponder?.reloadData() } - + + 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? { let array = list.filter({ $0.type == .original }) var currentSpeakerItem: VideoSeatItem? @@ -326,11 +296,11 @@ extension TUIVideoSeatViewModel { } return currentSpeakerItem } - + + //判断type状态 private func refreshListSeatItem() { sortSeatItems() listSeatItem = Array(videoSeatItems) - if videoSeatItems.count == 1 { // 单人 videoSeatViewType = .singleType @@ -354,7 +324,7 @@ extension TUIVideoSeatViewModel { refreshMultiVideo() } } - + private func refreshMultiVideo() { let screenResult = videoSeatItems.filter({ $0.hasScreenStream }) let videoResult = videoSeatItems.filter({ $0.hasVideoStream }) @@ -366,7 +336,7 @@ extension TUIVideoSeatViewModel { // 只有一路视频 speechItem = item } - + if let item = speechItem, let seatItemIndex = videoSeatItems.firstIndex(where: { $0.userId == item.userId }) { // 演讲者 videoSeatViewType = .speechType @@ -398,7 +368,7 @@ extension TUIVideoSeatViewModel { videoSeatViewType = .equallyDividedType } } - + private func reloadSeatItems() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -406,22 +376,26 @@ extension TUIVideoSeatViewModel { let lastListSeatItem = Array(self.listSeatItem) self.refreshListSeatItem() self.updateCollectionView(type, lastListSeatItem) - if self.videoSeatViewType == .largeSmallWindowType { - self.speakerItem = nil - self.shareItem = nil - self.viewResponder?.updateMiniscreen(self.smallItem) - } else if self.videoSeatViewType == .speechType { - self.smallItem = nil - self.viewResponder?.updateMiniscreen(self.speakerItem) - } else { - self.smallItem = nil - self.speakerItem = nil - self.shareItem = nil - self.viewResponder?.updateMiniscreen(nil) - } + self.resetMiniscreen() } } - + + private func resetMiniscreen() { + if self.videoSeatViewType == .largeSmallWindowType { + self.speakerItem = nil + self.shareItem = nil + self.viewResponder?.updateMiniscreen(self.smallItem) + } else if self.videoSeatViewType == .speechType { + self.smallItem = nil + self.viewResponder?.updateMiniscreen(self.speakerItem) + } else { + self.smallItem = nil + self.speakerItem = nil + self.shareItem = nil + self.viewResponder?.updateMiniscreen(nil) + } + } + private func updateSeatVolume(item: VideoSeatItem) { viewResponder?.updateSeatVolume(item) if let shareItem = shareItem, shareItem.userId == item.userId { @@ -429,20 +403,20 @@ extension TUIVideoSeatViewModel { shareItem.audioVolume = item.audioVolume viewResponder?.updateSeatVolume(shareItem) } - + if let speakerItem = speakerItem, speakerItem.userId == item.userId { speakerItem.hasAudioStream = item.hasAudioStream speakerItem.audioVolume = item.audioVolume viewResponder?.updateMiniscreenVolume(speakerItem) } - + if let smallItem = smallItem, smallItem.userId == item.userId { smallItem.hasAudioStream = item.hasAudioStream smallItem.audioVolume = item.audioVolume viewResponder?.updateMiniscreenVolume(smallItem) } } - + private func updateCollectionView(_ type: TUIVideoSeatViewType, _ lastList: [VideoSeatItem]) { if type != videoSeatViewType { viewResponder?.reloadData() @@ -455,23 +429,14 @@ extension TUIVideoSeatViewModel { indexPaths.append(IndexPath(item: i, section: 0)) } viewResponder?.insertItems(at: indexPaths) - } else if diffItem < 0 { - for i in (count + diffItem + 1) ... count { - indexPaths.append(IndexPath(item: i - 1, section: 0)) - } - viewResponder?.deleteItems(at: indexPaths) } - indexPaths = [] for i in 0 ... min(max(count - 1, 0), max(listSeatItem.count - 1, 0)) { if lastList.count > i && listSeatItem.count > i && lastList[i] != listSeatItem[i] { - indexPaths.append(IndexPath(item: i, section: 0)) + if let item = listSeatItem[safe: i] { + viewResponder?.updateSeatItem(item) + } } } - if indexPaths.count > 0 { - viewResponder?.reloadItems(at: indexPaths) - } else if diffItem == 0 && isNeedReloadStream { - viewResponder?.reloadData() - } } } @@ -483,20 +448,58 @@ extension TUIVideoSeatViewModel { } } -extension TUIVideoSeatViewModel: TUIRoomObserver { - func onUserAudioStateChanged(userId: String, hasAudio: Bool, reason: TUIChangeReason) { - guard let seatItem = getSeatItem(userId) else { - let userInfo = userNoExistMap[userId] ?? TUIUserInfo() - userInfo.hasAudioStream = hasAudio - userNoExistMap[userId] = userInfo - return +extension TUIVideoSeatViewModel: RoomKitUIEventResponder { + func onNotifyUIEvent(key: EngineEventCenter.RoomUIEvent, Object: Any?, info: [AnyHashable : Any]?) { + switch key { + case .TUIRoomKitService_RenewVideoSeatView: + initVideoSeatItems() + default: break } - // 同步刷新 分享和speaker的view的音量 - seatItem.hasAudioStream = hasAudio - updateSeatVolume(item: seatItem) } +} - public func onUserVoiceVolumeChanged(volumeMap: [String: NSNumber]) { +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.speechMode != .applySpeakAfterTakingSeat else { return } + addUserInfo(userInfo.userId) + case .onRemoteUserLeaveRoom: + guard let userInfo = param?["userInfo"] as? TUIUserInfo else { return } + removeSeatItem(userInfo.userId) + case .onUserRoleChanged: + engineManager.fetchRoomInfo() { [weak self] in + guard let self = self else { return } + self.reloadSeatItems() + } + 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 } @@ -505,7 +508,7 @@ extension TUIVideoSeatViewModel: TUIRoomObserver { seatItem.audioVolume = volume.intValue updateSeatVolume(item: seatItem) } - + if videoSeatViewType == .speechType { guard let currentSpeakerItem = findCurrentSpeaker(list: listSeatItem), currentSpeakerItem.hasAudioStream else { return } if speakerItem?.userId == currentSpeakerItem.userId { @@ -518,28 +521,19 @@ extension TUIVideoSeatViewModel: TUIRoomObserver { speakerItem = currentSpeakerItem } } - - public func onUserVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool, reason: TUIChangeReason) { + + private func userVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool) { if streamType == .screenStream, userId == currentUserId { - viewResponder?.showScreenCaptureMaskView(isShow: hasVideo) - return + viewResponder?.showScreenCaptureMaskView(isShow: hasVideo) + return } if hasVideo { let renderParams = TRTCRenderParams() renderParams.fillMode = (streamType == .screenStream) ? .fit : .fill let trtcStreamType: TRTCVideoStreamType = (streamType == .screenStream) ? .sub : .big - roomEngine?.getTRTCCloud().setRemoteRenderParams(userId, streamType: trtcStreamType, params: renderParams) - } - guard var seatItem = getSeatItem(userId) else { - let userInfo = userNoExistMap[userId] ?? TUIUserInfo() - if streamType == .screenStream { - userInfo.hasScreenStream = hasVideo - } else { - userInfo.hasVideoStream = hasVideo - } - userNoExistMap[userId] = userInfo - return + engineManager.setRemoteRenderParams(userId: userId, streamType: trtcStreamType, params: renderParams) } + guard var seatItem = getSeatItem(userId) else { return } if streamType == .cameraStream { seatItem.hasVideoStream = hasVideo viewResponder?.updateSeatItem(seatItem) @@ -550,47 +544,34 @@ extension TUIVideoSeatViewModel: TUIRoomObserver { } } if hasVideo { - startPlayVideo(item: seatItem, renderView: viewResponder?.getSeatVideoRenderView(seatItem)) + startPlayVideo(item: seatItem, renderView: viewResponder?.getVideoVisibleCell(seatItem)?.renderView) } else { stopPlayVideo(item: seatItem) } - reloadSeatItems() + if streamType == .screenStream, hasVideo, videoSeatItems.filter({ $0.hasScreenStream }).count == 1 { + refreshListSeatItem() + viewResponder?.insertItems(at: [IndexPath(item: 0, section: 0)]) + resetMiniscreen() + } else { + reloadSeatItems() + } } - + // seatList: 当前麦位列表 seated: 新增上麦的用户列表 left: 下麦的用户列表 - public func onSeatListChanged(seatList: [TUISeatInfo], seated: [TUISeatInfo], left: [TUISeatInfo]) { + private func seatListChanged(seated: [TUISeatInfo], left: [TUISeatInfo]) { for leftSeat in left { if let userId = leftSeat.userId { removeSeatItem(userId) } } - for seatInfo in seatList { - addSeatInfo(seatInfo) - } - reloadSeatItems() - } - - public func onUserRoleChanged(userId: String, userRole: TUIRole) { - guard let seatItem = getSeatItem(userId) else { return } - seatItem.userRole = userRole - reloadSeatItems() - } - - public func onRemoteUserEnterRoom(roomId: String, userInfo: TUIUserInfo) { - switch roomInfo.speechMode { - case .freeToSpeak: - addUserInfo(userInfo) - case .applySpeakAfterTakingSeat: - break - default: break + for seatInfo in seated { + if let userId = seatInfo.userId { + addUserInfo(userId) + } } } - - public func onRemoteUserLeaveRoom(roomId: String, userInfo: TUIUserInfo) { - removeSeatItem(userInfo.userId) - } - public func onUserScreenCaptureStopped(reason: Int) { + private func userScreenCaptureStopped() { viewResponder?.showScreenCaptureMaskView(isShow: false) guard let seatItem = getSeatItem(currentUserId) else { return } seatItem.hasScreenStream = false diff --git a/iOS/TUIRoomKit/Source/ViewModel/TransferMasterViewModel.swift b/iOS/TUIRoomKit/Source/ViewModel/TransferMasterViewModel.swift index 92575427e..1d41f9bd2 100644 --- a/iOS/TUIRoomKit/Source/ViewModel/TransferMasterViewModel.swift +++ b/iOS/TUIRoomKit/Source/ViewModel/TransferMasterViewModel.swift @@ -10,6 +10,7 @@ import Foundation protocol TransferMasterViewResponder: NSObject { func reloadTransferMasterTableView() func searchControllerChangeActive(isActive: Bool) + func makeToast(message: String) } class TransferMasterViewModel: NSObject { @@ -37,15 +38,17 @@ class TransferMasterViewModel: NSObject { guard userId != "" else { return } engineManager.changeUserRole(userId: userId, role: .roomOwner) { [weak self] in guard let self = self else { return } - self.engineManager.exitRoom(onSuccess: nil, onError: nil) - self.roomRouter.dismissAllRoomPopupViewController() - self.roomRouter.popToRoomEntranceViewController() + self.engineManager.exitRoom { [weak self] in + guard let self = self else { return } + self.roomRouter.dismissAllRoomPopupViewController() + self.roomRouter.popToRoomEntranceViewController() + } onError: { [weak self] code, message in + guard let self = self else { return } + self.viewResponder?.makeToast(message: message) + } } onError: { [weak self] code, message in guard let self = self else { return } - self.engineManager.destroyRoom(onSuccess: nil, onError: nil) - self.roomRouter.dismissAllRoomPopupViewController() - self.roomRouter.popToRoomEntranceViewController() - debugPrint("changeUserRole:code:\(code),message:\(message)") + self.viewResponder?.makeToast(message: message) } } diff --git a/iOS/TUIRoomKit/Source/ViewModel/UserListManagerViewModel.swift b/iOS/TUIRoomKit/Source/ViewModel/UserListManagerViewModel.swift index 79606ac72..dc102452e 100644 --- a/iOS/TUIRoomKit/Source/ViewModel/UserListManagerViewModel.swift +++ b/iOS/TUIRoomKit/Source/ViewModel/UserListManagerViewModel.swift @@ -13,6 +13,7 @@ protocol UserListManagerViewEventResponder: AnyObject { func updateUI(items: [ButtonItemData]) func makeToast(text: String) func showTransferredRoomOwnerAlert() + func showKickOutAlert(title: String, sureAction: (() ->())?) func setUserListManagerViewHidden(isHidden: Bool) } @@ -25,7 +26,7 @@ class UserListManagerViewModel: NSObject { } var userId: String = "" var userName: String = "" - let timeoutNumber: Double = 0 + let timeoutNumber: Double = 10 private(set) var otherUserItems: [ButtonItemData] = []//其他用户viewItem private(set) var currentUserItems: [ButtonItemData] = [] private(set) var onSeatItems: [ButtonItemData] = []//已经上麦的用户viewItem @@ -45,7 +46,6 @@ class UserListManagerViewModel: NSObject { } private var hasOpenCameraInvite = false private var hasOpenMicrophoneInvite = false - var hasTakeSeatInvite = false override init() { super.init() @@ -315,29 +315,20 @@ class UserListManagerViewModel: NSObject { func inviteSeatAction(sender: UIButton) { sender.isSelected = !sender.isSelected - if !hasTakeSeatInvite { - engineManager.takeUserOnSeatByAdmin(userId: userId, timeout: timeoutNumber) { [weak self] _,_ in - guard let self = self else { return } - self.hasTakeSeatInvite = false + if !engineManager.store.extendedInvitationList.contains(userId) { + engineManager.takeUserOnSeatByAdmin(userId: userId, timeout: timeoutNumber) { _,_ in } onRejected: { [weak self] requestId, userId, message in guard let self = self else { return } - self.hasTakeSeatInvite = false self.viewResponder?.makeToast(text: self.userName + .refusedTakeSeatInvitationText) - } onCancelled: { [weak self] _, _ in - guard let self = self else { return } - self.hasTakeSeatInvite = false } onTimeout: { [weak self] _, _ in guard let self = self else { return } - self.hasTakeSeatInvite = false self.viewResponder?.makeToast(text: .takeSeatInvitationTimeoutText) } onError: { [weak self] _, _, _, message in guard let self = self else { return } - self.hasTakeSeatInvite = false self.viewResponder?.makeToast(text: message) } } viewResponder?.makeToast(text: .invitedTakeSeatText) - hasTakeSeatInvite = true hideUserListManagerView() } @@ -368,8 +359,12 @@ class UserListManagerViewModel: NSObject { func kickOutAction(sender: UIButton) { sender.isSelected = !sender.isSelected - engineManager.kickRemoteUserOutOfRoom(userId: userId) - hideUserListManagerView() + let kickOutTitle = localizedReplace(.kickOutText, replace: userName) + viewResponder?.showKickOutAlert(title: kickOutTitle, sureAction: { [weak self] in + guard let self = self else { return } + self.engineManager.kickRemoteUserOutOfRoom(userId: self.userId) + self.hideUserListManagerView() + }) } //根据用户的状态更新item数组 @@ -542,4 +537,7 @@ private extension String { static var invitedOpenVideoText: String { localized("TUIRoom.invited.open.video") } + static var kickOutText: String { + localized("TUIRoom.sure.kick.out") + } } diff --git a/iOS/TUIRoomKit/Source/ViewModel/UserListViewModel.swift b/iOS/TUIRoomKit/Source/ViewModel/UserListViewModel.swift index 00b08fd2b..d10322222 100644 --- a/iOS/TUIRoomKit/Source/ViewModel/UserListViewModel.swift +++ b/iOS/TUIRoomKit/Source/ViewModel/UserListViewModel.swift @@ -100,6 +100,7 @@ class UserListViewModel: NSObject { } func inviteSeatAction(sender: UIButton) { + userManagerModel.userId = userId userManagerModel.inviteSeatAction(sender: sender) } } diff --git a/iOS/TUIRoomKit/TUIRoomKit.podspec b/iOS/TUIRoomKit/TUIRoomKit.podspec index 8923d22cc..c2ff62746 100644 --- a/iOS/TUIRoomKit/TUIRoomKit.podspec +++ b/iOS/TUIRoomKit/TUIRoomKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'TUIRoomKit' - spec.version = '1.6.1' + spec.version = '1.7.0' spec.platform = :ios spec.ios.deployment_target = '13.0' spec.license = { :type => 'MIT', :file => 'LICENSE' } @@ -23,7 +23,7 @@ Pod::Spec.new do |spec| spec.default_subspec = 'TRTC' spec.subspec 'Professional' do |professional| - professional.dependency 'TUIRoomEngine/Professional', '1.6.1' + professional.dependency 'TUIRoomEngine/Professional', '1.7.0' professional.source_files = 'Source/*.swift', 'Source/Presenter/*.swift', 'Source/**/*.swift', 'Source/**/*.h', 'Source/**/*.m', 'RoomExtension/**/*.swift', 'RoomExtension/**/*.h', 'RoomExtension/**/*.m' professional.resource_bundles = { 'TUIRoomKitBundle' => ['Resources/*.xcassets', 'Resources/Localized/**/*.strings'] @@ -32,7 +32,7 @@ Pod::Spec.new do |spec| end spec.subspec 'TRTC' do |trtc| - trtc.dependency 'TUIRoomEngine/TRTC', '1.6.1' + trtc.dependency 'TUIRoomEngine/TRTC', '1.7.0' trtc.source_files = 'Source/*.swift', 'Source/Presenter/*.swift', 'Source/**/*.swift', 'Source/**/*.h', 'Source/**/*.m', 'RoomExtension/**/*.swift', 'RoomExtension/**/*.h', 'RoomExtension/**/*.m' trtc.resource_bundles = { 'TUIRoomKitBundle' => ['Resources/*.xcassets', 'Resources/Localized/**/*.strings']