增加换肤功能

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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