增加换肤功能

This commit is contained in:
启星
2025-08-14 10:07:49 +08:00
parent f6964c1e89
commit 4f9318d98e
8789 changed files with 978530 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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