增加换肤功能
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
//
|
||||
// ConferenceInvitationViewController.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by jeremiawang on 2024/8/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RTCRoomEngine
|
||||
import UIKit
|
||||
import Combine
|
||||
import Factory
|
||||
|
||||
class ConferenceInvitationViewController: UIViewController {
|
||||
private var cancellableSet = Set<AnyCancellable>()
|
||||
var roomInfo: TUIRoomInfo
|
||||
var invitation: TUIInvitation
|
||||
|
||||
init(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
|
||||
self.roomInfo = roomInfo
|
||||
self.invitation = invitation
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.modalPresentationStyle = .fullScreen
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public var shouldAutorotate: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
private let backgroundImageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.contentMode = .scaleAspectFill
|
||||
view.alpha = 0.2
|
||||
return view
|
||||
}()
|
||||
|
||||
private let avatarImageView: UIImageView = {
|
||||
let avatarImageView = UIImageView()
|
||||
avatarImageView.contentMode = .scaleAspectFill
|
||||
avatarImageView.layer.cornerRadius = 25
|
||||
avatarImageView.clipsToBounds = true
|
||||
return avatarImageView
|
||||
}()
|
||||
|
||||
private let inviteLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .white
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
|
||||
return label
|
||||
}()
|
||||
|
||||
private let conferenceNameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .white
|
||||
label.font = UIFont.boldSystemFont(ofSize: 24)
|
||||
label.numberOfLines = 1
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
return label
|
||||
}()
|
||||
|
||||
private let detailsLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = .white
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
|
||||
return label
|
||||
}()
|
||||
|
||||
private let joinSliderView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white.withAlphaComponent(0.1)
|
||||
view.layer.cornerRadius = 39.scale375()
|
||||
return view
|
||||
}()
|
||||
|
||||
private let joinLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .joinNowText
|
||||
label.textColor = .white.withAlphaComponent(0.8)
|
||||
label.font = UIFont(name: "PingFangSC-Medium", size: 16)
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
private let sliderThumbView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.tui_color(withHex: "1C66E5")
|
||||
view.layer.cornerRadius = 32
|
||||
return view
|
||||
}()
|
||||
|
||||
private let arrowImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.image = UIImage(named: "room_rightlink_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
imageView.tintColor = .white
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let declineButton: UIButton = {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(.notEnterText, for: .normal)
|
||||
button.setTitleColor(.white.withAlphaComponent(0.8), for: .normal)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Medium", size: 16)
|
||||
return button
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
initState()
|
||||
initializeData()
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
operation.select(ViewSelectors.getDismissInvitationFlag)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] shouldDismissInvitation in
|
||||
guard let self = self else { return }
|
||||
if shouldDismissInvitation {
|
||||
InvitationObserverService.shared.dismissInvitationWindow()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
|
||||
private func initState() {
|
||||
self.operation.dispatch(action: InvitationViewActions.resetInvitationFlag())
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
view.addSubview(backgroundImageView)
|
||||
view.addSubview(avatarImageView)
|
||||
view.addSubview(inviteLabel)
|
||||
view.addSubview(conferenceNameLabel)
|
||||
view.addSubview(detailsLabel)
|
||||
view.addSubview(joinSliderView)
|
||||
joinSliderView.addSubview(joinLabel)
|
||||
joinSliderView.addSubview(sliderThumbView)
|
||||
sliderThumbView.addSubview(arrowImageView)
|
||||
view.addSubview(declineButton)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
backgroundImageView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.height.width.equalTo(50.scale375())
|
||||
make.top.equalToSuperview().offset(150.scale375Height())
|
||||
}
|
||||
inviteLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalTo(avatarImageView.snp.bottom).offset(16)
|
||||
}
|
||||
conferenceNameLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.width.lessThanOrEqualTo(300.scale375())
|
||||
make.top.equalTo(inviteLabel.snp.bottom).offset(30)
|
||||
}
|
||||
detailsLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalTo(conferenceNameLabel.snp.bottom).offset(10)
|
||||
}
|
||||
declineButton.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset(-94.scale375Height())
|
||||
}
|
||||
joinSliderView.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.width.equalTo(200.scale375())
|
||||
make.height.equalTo(78.scale375())
|
||||
make.bottom.equalTo(declineButton.snp.top).offset(-30)
|
||||
}
|
||||
joinLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalToSuperview()
|
||||
make.centerX.equalToSuperview().offset(32.scale375())
|
||||
}
|
||||
sliderThumbView.snp.makeConstraints { make in
|
||||
make.left.equalTo(joinSliderView.snp.left).offset(5)
|
||||
make.centerY.equalTo(joinSliderView.snp.centerY)
|
||||
make.width.height.equalTo(64.scale375())
|
||||
}
|
||||
arrowImageView.snp.makeConstraints { make in
|
||||
make.center.equalTo(sliderThumbView)
|
||||
make.width.height.equalTo(20.scale375())
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
|
||||
sliderThumbView.addGestureRecognizer(panGesture)
|
||||
declineButton.addTarget(self, action: #selector(rejectAction), for: .touchUpInside)
|
||||
}
|
||||
|
||||
private func initializeData() {
|
||||
inviteLabel.text = invitation.inviter.userName + .inviteJoinConferenceText
|
||||
conferenceNameLabel.text = roomInfo.name
|
||||
detailsLabel.text = .hostText + roomInfo.ownerName + " | " + .participantText + String(roomInfo.memberCount)
|
||||
|
||||
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
if let url = URL(string: invitation.inviter.avatarUrl) {
|
||||
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
|
||||
backgroundImageView.sd_setImage(with: url, placeholderImage: placeholder)
|
||||
} else {
|
||||
avatarImageView.image = placeholder
|
||||
backgroundImageView.image = placeholder
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
|
||||
let translation = gesture.translation(in: joinSliderView)
|
||||
let maxTranslation = joinSliderView.frame.width - sliderThumbView.frame.width - 10
|
||||
|
||||
switch gesture.state {
|
||||
case .changed:
|
||||
if translation.x >= 0 && translation.x <= maxTranslation {
|
||||
sliderThumbView.snp.updateConstraints { make in
|
||||
make.left.equalTo(joinSliderView.snp.left).offset(5 + translation.x)
|
||||
}
|
||||
}
|
||||
case .ended:
|
||||
if translation.x >= maxTranslation {
|
||||
sliderThumbView.snp.updateConstraints { make in
|
||||
make.left.equalTo(joinSliderView.snp.left).offset(5 + maxTranslation)
|
||||
}
|
||||
gesture.isEnabled = false
|
||||
self.acceptAction()
|
||||
} else {
|
||||
sliderThumbView.snp.updateConstraints { make in
|
||||
make.left.equalTo(joinSliderView.snp.left).offset(5)
|
||||
}
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func acceptAction() {
|
||||
operation.dispatch(action: ConferenceInvitationActions.accept(payload: roomInfo.roomId))
|
||||
}
|
||||
|
||||
@objc func rejectAction() {
|
||||
operation.dispatch(action: InvitationViewActions.dismissInvitationView())
|
||||
operation.dispatch(action: ConferenceInvitationActions.reject(payload: (roomInfo.roomId, .rejectToEnter)))
|
||||
}
|
||||
|
||||
@Injected(\.navigation) var route
|
||||
@Injected(\.conferenceStore) var operation
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var inviteJoinConferenceText: String {
|
||||
localized(" invite you to join the conference")
|
||||
}
|
||||
static var hostText: String {
|
||||
localized("Conference Host")
|
||||
}
|
||||
static var participantText: String {
|
||||
localized("Participant")
|
||||
}
|
||||
static var peopleText: String {
|
||||
localized("People")
|
||||
}
|
||||
static var joinNowText: String {
|
||||
localized("Join now")
|
||||
}
|
||||
static var notEnterText: String {
|
||||
localized("Do not enter for now")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//
|
||||
// ConferenceOptionCell.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by CY zhao on 2024/6/6.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Factory
|
||||
|
||||
class ConferenceListCell: UITableViewCell {
|
||||
@Injected(\.conferenceStore) private var store
|
||||
@Injected(\.conferenceMainViewStore) private var viewStore
|
||||
static let reusedIdentifier = "ConferenceListCell"
|
||||
private var conferenceInfo: ConferenceInfo?
|
||||
|
||||
let roomNameLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = UIFont(name: "PingFangSC-Medium", size: 16)
|
||||
label.textColor = UIColor.tui_color(withHex: "4F586B")
|
||||
return label
|
||||
}()
|
||||
|
||||
let interactiveIcon: UIImageView = {
|
||||
let image = UIImage(named: "room_right_black_arrow", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
let imageView = UIImageView(image: image)
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let detailLabel: UILabel = {
|
||||
let label = UILabel(frame: .zero)
|
||||
return label
|
||||
}()
|
||||
|
||||
let enterButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.setTitle(.enterText, for: .normal)
|
||||
button.setTitleColor(UIColor.tui_color(withHex: "#4E5461"), for: .normal)
|
||||
button.titleLabel?.font = UIFont(name: "PingFangSC-Medium", size: 14)
|
||||
button.backgroundColor = UIColor.tui_color(withHex: "F0F3FA")
|
||||
button.sizeToFit()
|
||||
button.layer.cornerRadius = button.frame.height / 2
|
||||
return button
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(roomNameLabel)
|
||||
contentView.addSubview(interactiveIcon)
|
||||
contentView.addSubview(detailLabel)
|
||||
contentView.addSubview(enterButton)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
enterButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview()
|
||||
make.width.greaterThanOrEqualTo(68)
|
||||
make.top.equalToSuperview().offset(8)
|
||||
}
|
||||
interactiveIcon.snp.makeConstraints { make in
|
||||
make.trailing.lessThanOrEqualTo(enterButton.snp.leading).offset(-5)
|
||||
make.width.height.equalTo(16)
|
||||
make.centerY.equalTo(roomNameLabel)
|
||||
}
|
||||
roomNameLabel.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(interactiveIcon.snp.leading)
|
||||
make.leading.equalToSuperview()
|
||||
make.top.equalToSuperview()
|
||||
}
|
||||
detailLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview()
|
||||
make.top.equalTo(roomNameLabel.snp.bottom).offset(6)
|
||||
make.trailing.lessThanOrEqualTo(enterButton.snp.leading).offset(-20)
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
enterButton.addTarget(self, action: #selector(enterAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func enterAction(sender: UIButton) {
|
||||
guard let info = conferenceInfo else {
|
||||
return
|
||||
}
|
||||
if !info.basicInfo.roomId.isEmpty {
|
||||
store.dispatch(action: RoomActions.joinConference(payload: info.basicInfo.roomId))
|
||||
store.dispatch(action: ScheduleViewActions.popDetailView())
|
||||
viewStore.updateInternalCreation(isInternalCreation: true)
|
||||
}
|
||||
}
|
||||
|
||||
func updateCell(with info: ConferenceInfo) {
|
||||
conferenceInfo = info
|
||||
roomNameLabel.text = info.basicInfo.name
|
||||
detailLabel.attributedText = getAttributedText(from: info)
|
||||
}
|
||||
|
||||
private func getAttributedText(from info: ConferenceInfo) -> NSMutableAttributedString {
|
||||
let normalAttributes: [NSAttributedString.Key: Any] =
|
||||
[.font: UIFont.systemFont(ofSize: 14), .foregroundColor: UIColor.tui_color(withHex: "4F586B")]
|
||||
let duration = getDuration(from: info)
|
||||
var result = NSMutableAttributedString(string: duration, attributes: normalAttributes)
|
||||
|
||||
addDelimiter(to: &result)
|
||||
let roomId = addSpaces(to: info.basicInfo.roomId)
|
||||
let roomIdAtrributeString = NSMutableAttributedString(string: roomId, attributes: normalAttributes)
|
||||
result.append(roomIdAtrributeString)
|
||||
|
||||
guard info.status == .running else { return result }
|
||||
|
||||
addDelimiter(to: &result)
|
||||
let status = getStatusString(from: info)
|
||||
let statusAttributes: [NSAttributedString.Key: Any] =
|
||||
[.font:UIFont.systemFont(ofSize: 14), .foregroundColor: UIColor.tui_color(withHex: "1C66E5")]
|
||||
let statusAtrributeString = NSMutableAttributedString(string: status, attributes: statusAttributes)
|
||||
result.append(statusAtrributeString)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func addDelimiter(to attributeString: inout NSMutableAttributedString) {
|
||||
let delimiterAtrributeString = NSMutableAttributedString(string:" | ",
|
||||
attributes: [
|
||||
.font: UIFont.systemFont(ofSize: 11),
|
||||
.foregroundColor: UIColor.tui_color(withHex: "969EB4"),
|
||||
.baselineOffset: 2
|
||||
])
|
||||
attributeString.append(delimiterAtrributeString)
|
||||
}
|
||||
|
||||
private func addSpaces(to string: String) -> String {
|
||||
var result = ""
|
||||
for (index, char) in string.enumerated() {
|
||||
if index > 0 && index % 3 == 0 {
|
||||
result += " "
|
||||
}
|
||||
result += String(char)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func getDuration(from info: ConferenceInfo) -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.timeZone = .current
|
||||
dateFormatter.dateFormat = "HH:mm"
|
||||
|
||||
let startDate = Date(timeIntervalSince1970: TimeInterval(info.scheduleStartTime))
|
||||
let endDate = Date(timeIntervalSince1970: TimeInterval(info.scheduleEndTime))
|
||||
|
||||
let startString = dateFormatter.string(from: startDate)
|
||||
let endString = dateFormatter.string(from: endDate)
|
||||
return startString + " - " + endString
|
||||
}
|
||||
|
||||
private func getStatusString(from info: ConferenceInfo) -> String {
|
||||
if info.status == .running {
|
||||
return .inProgressText
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var enterText: String {
|
||||
localized("Enter")
|
||||
}
|
||||
static var inProgressText: String {
|
||||
localized("Ongoing")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
//
|
||||
// SelectedMembersViewController.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by CY zhao on 2024/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RTCRoomEngine
|
||||
|
||||
protocol SelectedMemberCellProtocol: AnyObject {
|
||||
func didDeleteButtonClicked(in memberCell: SelectedMemberCell)
|
||||
}
|
||||
|
||||
class SelectedMembersViewController: UIViewController {
|
||||
private(set) var showDeleteButton: Bool = true
|
||||
var selectedMember: [UserInfo] = []
|
||||
var didDeselectMember: ((UserInfo) -> Void)?
|
||||
private let arrowViewHeight: CGFloat = 35.0
|
||||
|
||||
init(showDeleteButton: Bool = true, selectedMembers: [UserInfo] = []) {
|
||||
self.showDeleteButton = showDeleteButton
|
||||
self.selectedMember = selectedMembers
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modalPresentationStyle = .custom
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private let tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.register(SelectedMemberCell.self, forCellReuseIdentifier: SelectedMemberCell.reuseIdentifier)
|
||||
if #available(iOS 15.0, *) {
|
||||
tableView.sectionHeaderTopPadding = 0
|
||||
}
|
||||
return tableView
|
||||
}()
|
||||
|
||||
private let dropArrowView : UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .white
|
||||
return view
|
||||
}()
|
||||
|
||||
private let dropArrowImageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.image = UIImage(named: "room_drop_arrow", in:tuiRoomKitBundle(), compatibleWith: nil)
|
||||
return view
|
||||
}()
|
||||
|
||||
let contentView: UIView = {
|
||||
let view = UIView(frame: .zero)
|
||||
view.backgroundColor = .white
|
||||
view.layer.cornerRadius = 8
|
||||
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
view.clipsToBounds = true
|
||||
return view
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = UIColor.tui_color(withHex: "0F1014", alpha: 0.6)
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
view.addSubview(contentView)
|
||||
dropArrowView.addSubview(dropArrowImageView)
|
||||
contentView.addSubview(dropArrowView)
|
||||
contentView.addSubview(tableView)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
contentView.snp.makeConstraints { make in
|
||||
make.height.equalTo(610)
|
||||
make.leading.bottom.trailing.equalToSuperview()
|
||||
}
|
||||
dropArrowView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(arrowViewHeight)
|
||||
}
|
||||
dropArrowImageView.snp.makeConstraints { make in
|
||||
make.centerX.centerY.equalToSuperview()
|
||||
make.width.equalTo(24.scale375())
|
||||
make.height.equalTo(3.scale375())
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(dropArrowView.snp.bottom)
|
||||
make.leading.bottom.trailing.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
let dropArrowTap = UITapGestureRecognizer(target: self, action: #selector(dropDownPopUpViewAction(sender:)))
|
||||
dropArrowView.addGestureRecognizer(dropArrowTap)
|
||||
dropArrowView.isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
@objc func dropDownPopUpViewAction(sender: UIView) {
|
||||
self.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension SelectedMembersViewController: SelectedMemberCellProtocol{
|
||||
func didDeleteButtonClicked(in memberCell: SelectedMemberCell) {
|
||||
guard let indexPath = self.tableView.indexPath(for: memberCell) else {
|
||||
return
|
||||
}
|
||||
let member = selectedMember[indexPath.row]
|
||||
self.didDeselectMember?(member)
|
||||
selectedMember.remove(at: indexPath.row)
|
||||
self.tableView.deleteRows(at: [indexPath], with: .none)
|
||||
|
||||
guard let headerView = tableView.headerView(forSection: 0) as? SelectedMemberHeaderView else {
|
||||
return
|
||||
}
|
||||
headerView.updateLabel(with: selectedMember.count)
|
||||
}
|
||||
}
|
||||
|
||||
extension SelectedMembersViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 52
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
let headerView = SelectedMemberHeaderView(reuseIdentifier: "CustomHeaderView")
|
||||
headerView.updateLabel(with: selectedMember.count)
|
||||
return headerView
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return 38
|
||||
}
|
||||
}
|
||||
|
||||
extension SelectedMembersViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return selectedMember.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: SelectedMemberCell.reuseIdentifier, for: indexPath)
|
||||
as? SelectedMemberCell else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
let member = selectedMember[indexPath.row]
|
||||
cell.updateView(with: member)
|
||||
cell.delegate = self
|
||||
if showDeleteButton {
|
||||
cell.showDeleteButton()
|
||||
} else {
|
||||
cell.hideDeleteButton()
|
||||
}
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
class SelectedMemberCell: UITableViewCell {
|
||||
static let reuseIdentifier = "SelectedMemberCell"
|
||||
weak var delegate: SelectedMemberCellProtocol?
|
||||
private let avatarImageView: UIImageView = {
|
||||
let imgView = UIImageView()
|
||||
imgView.layer.cornerRadius = 2
|
||||
imgView.layer.masksToBounds = true
|
||||
return imgView
|
||||
}()
|
||||
|
||||
private let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor.tui_color(withHex: "22262E")
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
label.font = UIFont(name: "PingFangSC-Regular", size: 14)
|
||||
label.numberOfLines = 1
|
||||
return label
|
||||
}()
|
||||
|
||||
let deleteButton: UIButton = {
|
||||
let button = LargerHitAreaButton()
|
||||
let image = UIImage(named: "room_delete", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
button.setImage(image, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
private var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
selectionStyle = .none
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
contentView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(avatarImageView)
|
||||
contentView.addSubview(nameLabel)
|
||||
contentView.addSubview(deleteButton)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(32)
|
||||
make.leading.equalToSuperview().offset(20)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(self.avatarImageView.snp.trailing).offset(5)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
deleteButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-20)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(16)
|
||||
}
|
||||
}
|
||||
|
||||
func updateView(with info: UserInfo) {
|
||||
let placeholder = UIImage(named: "room_default_avatar_rect", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
if let url = URL(string: info.avatarUrl) {
|
||||
avatarImageView.sd_setImage(with: url, placeholderImage: placeholder)
|
||||
} else {
|
||||
avatarImageView.image = placeholder
|
||||
}
|
||||
|
||||
if !info.userName.isEmpty {
|
||||
nameLabel.text = info.userName
|
||||
} else {
|
||||
nameLabel.text = info.userId
|
||||
}
|
||||
}
|
||||
|
||||
func hideDeleteButton() {
|
||||
self.deleteButton.isHidden = true
|
||||
}
|
||||
|
||||
func showDeleteButton() {
|
||||
self.deleteButton.isHidden = false
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
deleteButton.addTarget(self, action: #selector(deleteButtonTapped(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func deleteButtonTapped(sender: UIButton) {
|
||||
self.delegate?.didDeleteButtonClicked(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
class SelectedMemberHeaderView: UITableViewHeaderFooterView {
|
||||
override init(reuseIdentifier: String?) {
|
||||
super.init(reuseIdentifier: reuseIdentifier)
|
||||
backgroundView = UIView()
|
||||
backgroundView?.backgroundColor = .white
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private let label: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor.tui_color(withHex: "4F586B")
|
||||
label.font = UIFont(name: "PingFangSC-Medium", size: 18)
|
||||
label.text = .selectedText
|
||||
label.sizeToFit()
|
||||
return label
|
||||
}()
|
||||
|
||||
private var isViewReady = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else {
|
||||
return
|
||||
}
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(label)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
label.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(20)
|
||||
make.top.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func updateLabel(with count: Int) {
|
||||
let text = .selectedText + " (" + "\(count)" + ")"
|
||||
self.label.text = text
|
||||
}
|
||||
}
|
||||
|
||||
class LargerHitAreaButton: UIButton {
|
||||
var hitAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
let largerFrame = bounds.inset(by: hitAreaEdgeInsets)
|
||||
return largerFrame.contains(point)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var selectedText: String {
|
||||
localized("Selected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// SelectMemberViewFactory.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by CY zhao on 2024/6/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RTCRoomEngine
|
||||
|
||||
struct MemberSelectParams {
|
||||
let participants: ConferenceParticipants
|
||||
let delegate: ContactViewSelectDelegate
|
||||
let factory: MemberSelectionFactory
|
||||
}
|
||||
|
||||
@objc public protocol ContactViewProtocol: AnyObject {
|
||||
var delegate: ContactViewSelectDelegate? { get set }
|
||||
}
|
||||
|
||||
@objc public protocol ContactViewSelectDelegate: AnyObject {
|
||||
func onMemberSelected(_ viewController: ContactViewProtocol, invitees: [User])
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// ModifyScheduleDataHelper.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RTCRoomEngine
|
||||
|
||||
class ModifyScheduleDataHelper: ScheduleConferenceDataHelper {
|
||||
class func generateScheduleConferenceData(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore: ScheduleConferenceStore, viewController: ContactViewSelectDelegate) ->
|
||||
[Int : [CellConfigItem]] {
|
||||
var menus: [Int:[CellConfigItem]] = [:]
|
||||
menus[0] = getFirstSectionModifyMenus(route: route, store: modifyStore, viewController: viewController)
|
||||
menus[1] = getSecondSectionModifyMenus(route: route, store: store, operation: operation, modifyStore: modifyStore)
|
||||
return menus
|
||||
}
|
||||
|
||||
private class func getFirstSectionModifyMenus(route: Route, store: ScheduleConferenceStore, viewController: ContactViewSelectDelegate) -> [CellConfigItem] {
|
||||
var array: [CellConfigItem] = []
|
||||
array.append(getConferenceNameItem(route: route, store: store))
|
||||
var conferenceTypeItem = getConferenceTypeItem(route: route, store: store)
|
||||
conferenceTypeItem.showButton = false
|
||||
conferenceTypeItem.selectClosure = nil
|
||||
array.append(conferenceTypeItem)
|
||||
array.append(getModifyStartTimeItem(route: route, store: store))
|
||||
array.append(getModifyDurationTimeItem(route: route, store: store))
|
||||
array.append(getTimeZoneItem(route: route, store: store))
|
||||
array.append(getParticipatingMembersItem(route: route, store: store, viewController: viewController))
|
||||
return array
|
||||
}
|
||||
|
||||
private class func getSecondSectionModifyMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore:
|
||||
ScheduleConferenceStore) -> [CellConfigItem] {
|
||||
return [getSaveItem(route: route, store: store, operation: operation, modifyStore: modifyStore)]
|
||||
}
|
||||
|
||||
private class func getModifyStartTimeItem(route: Route, store: ScheduleConferenceStore) -> CellConfigItem {
|
||||
var startTimeItem = getStartTimeItem(route: route, store: store)
|
||||
startTimeItem.selectClosure = {
|
||||
let view = TimePickerView()
|
||||
view.pickerDate = Date(timeIntervalSince1970: TimeInterval(store.conferenceInfo.scheduleStartTime))
|
||||
view.dismissAction = {
|
||||
route.dismiss(animated: true)
|
||||
}
|
||||
route.present(route: .popup(view: view))
|
||||
}
|
||||
return startTimeItem
|
||||
}
|
||||
|
||||
private class func getModifyDurationTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
var durationTimeItem = getDurationTimeItem(route: route, store: store)
|
||||
durationTimeItem.selectClosure = {
|
||||
let view = DurationPickerView()
|
||||
view.timeDuration = TimeInterval(store.conferenceInfo.durationTime)
|
||||
view.dismissAction = {
|
||||
route.dismiss(animated: true)
|
||||
}
|
||||
route.present(route: .popup(view: view))
|
||||
}
|
||||
return durationTimeItem
|
||||
}
|
||||
|
||||
private class func getSaveItem(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore: ScheduleConferenceStore) -> CellConfigItem {
|
||||
var item = ButtonItem(title: .saveText)
|
||||
item.titleColor = UIColor(0xFFFFFF)
|
||||
item.backgroudColor = UIColor(0x1C66E5)
|
||||
item.selectClosure = {
|
||||
guard modifyStore.conferenceInfo.basicInfo.name.count > 0 else {
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .nameCannotBeEmptyText)))
|
||||
return
|
||||
}
|
||||
let currentList = operation.selectCurrent(ConferenceListSelectors.getConferenceList)
|
||||
let status = currentList.first(where: { $0.basicInfo.roomId == store.conferenceInfo.basicInfo.roomId })?.status
|
||||
guard status != .running else {
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .conferenceCannotBeModifiedText)))
|
||||
route.pop()
|
||||
return
|
||||
}
|
||||
updateConferenceInfoIfNeeded(store: store, operation: operation, modifyStore: modifyStore)
|
||||
updateAttendsIfNeeded(store: store, operation: operation, modifyStore: modifyStore)
|
||||
store.update(conference: modifyStore.conferenceInfo)
|
||||
route.pop()
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
private class func updateConferenceInfoIfNeeded(store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore: ScheduleConferenceStore) {
|
||||
var modifyFlag: TUIConferenceModifyFlag = []
|
||||
if modifyStore.conferenceInfo.basicInfo.name != store.conferenceInfo.basicInfo.name {
|
||||
modifyFlag = modifyFlag.union(.roomName)
|
||||
}
|
||||
if modifyStore.conferenceInfo.scheduleStartTime != store.conferenceInfo.scheduleStartTime ||
|
||||
modifyStore.conferenceInfo.durationTime != store.conferenceInfo.durationTime {
|
||||
modifyFlag = modifyFlag.union([.scheduleStartTime, .scheduleEndTime])
|
||||
}
|
||||
if !modifyFlag.isEmpty {
|
||||
let info = TUIConferenceInfo(conferenceInfo: modifyStore.conferenceInfo)
|
||||
operation.dispatch(action: ConferenceListActions.updateConferenceInfo(payload: (info, modifyFlag)))
|
||||
}
|
||||
}
|
||||
|
||||
private class func updateAttendsIfNeeded(store: ScheduleConferenceStore, operation: ConferenceStore, modifyStore: ScheduleConferenceStore){
|
||||
let conferenceId = store.conferenceInfo.basicInfo.roomId
|
||||
let newAttendeeSet = Set(modifyStore.conferenceInfo.attendeeListResult.attendeeList)
|
||||
let oldAttendeeSet = Set(store.conferenceInfo.attendeeListResult.attendeeList)
|
||||
let addList = newAttendeeSet.subtracting(oldAttendeeSet).map { $0.userId }
|
||||
let removeList = oldAttendeeSet.subtracting(newAttendeeSet).map { $0.userId }
|
||||
if !addList.isEmpty {
|
||||
operation.dispatch(action: ConferenceListActions.addAttendeesByAdmin(payload: (conferenceId, addList)))
|
||||
}
|
||||
if !removeList.isEmpty {
|
||||
operation.dispatch(action: ConferenceListActions.removeAttendeesByAdmin(payload: (conferenceId, removeList)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static let saveText = localized("Save")
|
||||
static let nameCannotBeEmptyText = localized("Conference name cannot be empty!")
|
||||
static let conferenceCannotBeModifiedText = localized("Conference has already started, and it cannot be modified!")
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// ModifyScheduleViewController.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
import Combine
|
||||
import TUICore
|
||||
|
||||
class ModifyScheduleViewController: UIViewController {
|
||||
private var cancellableSet = Set<AnyCancellable>()
|
||||
var conferenceInfo: ConferenceInfo
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
init(conferenceInfo: ConferenceInfo) {
|
||||
self.conferenceInfo = conferenceInfo
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
modifyStore.update(conference: self.conferenceInfo)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
self.view = ScheduleConferenceTableView(menus: ModifyScheduleDataHelper.generateScheduleConferenceData(route: route, store: store, operation: operation, modifyStore: modifyStore, viewController: self))
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
navigationItem.title = .modifyRoomText
|
||||
subscribeScheduleSubject()
|
||||
subscribeToast()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
|
||||
@Injected(\.navigation) private var route
|
||||
@Injected(\.scheduleStore) private var store
|
||||
@Injected(\.conferenceStore) private var operation
|
||||
@Injected(\.modifyScheduleStore) private var modifyStore
|
||||
}
|
||||
|
||||
extension ModifyScheduleViewController {
|
||||
private func subscribeScheduleSubject() {
|
||||
operation.scheduleActionSubject
|
||||
.receive(on: RunLoop.main)
|
||||
.filter { $0.id == ScheduleResponseActions.onUpdateInfoSuccess.id }
|
||||
.sink { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.route.pop()
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
|
||||
private func subscribeToast() {
|
||||
operation.toastSubject
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] toast in
|
||||
guard let self = self else { return }
|
||||
var position = TUICSToastPositionBottom
|
||||
switch toast.position {
|
||||
case .center:
|
||||
position = TUICSToastPositionCenter
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.view.makeToast(toast.message, duration: toast.duration, position: position)
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
}
|
||||
|
||||
extension ModifyScheduleViewController: ContactViewSelectDelegate {
|
||||
public func onMemberSelected(_ viewController: ContactViewProtocol,
|
||||
invitees: [User]) {
|
||||
var conferenceInfo = modifyStore.conferenceInfo
|
||||
conferenceInfo.attendeeListResult.attendeeList = invitees.map { $0.userInfo }
|
||||
conferenceInfo.attendeeListResult.totalCount = UInt(invitees.count)
|
||||
modifyStore.update(conference: conferenceInfo)
|
||||
route.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var modifyRoomText: String {
|
||||
localized("Modify Room")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// ScheduleConference+Injection.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by aby on 2024/6/27.
|
||||
//
|
||||
|
||||
import Factory
|
||||
|
||||
extension Container {
|
||||
var scheduleStore: Factory<ScheduleConferenceStore> {
|
||||
self {
|
||||
ScheduleConferenceStoreProvider()
|
||||
}
|
||||
.shared
|
||||
}
|
||||
var modifyScheduleStore: Factory<ScheduleConferenceStore> {
|
||||
self {
|
||||
ScheduleConferenceStoreProvider()
|
||||
}
|
||||
.shared
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// ScheduleConferenceStore.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by aby on 2024/6/27.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
protocol ScheduleConferenceStore {
|
||||
func fetchAttendees(cursor: String)
|
||||
func update(conference info: ConferenceInfo)
|
||||
func fetchRoomInfo(roomId: String)
|
||||
func select<Value:Equatable>(_ selector: Selector<ConferenceInfo, Value>) -> AnyPublisher<Value, Never>
|
||||
var conferenceInfo: ConferenceInfo { get }
|
||||
}
|
||||
|
||||
class ScheduleConferenceStoreProvider {
|
||||
static let updateConferenceInfo = ActionTemplate(id: "updateConferenceInfo", payloadType: ConferenceInfo.self)
|
||||
static let fetchAttendeeList = ActionTemplate(id: "fetchAttendeeList", payloadType: (String, String, Int).self)
|
||||
static let updateAttendeeList = ActionTemplate(id: "updateAttendeeList", payloadType: ([UserInfo], String, UInt).self)
|
||||
static let updateBasicInfo = ActionTemplate(id: "updateBasicInfo", payloadType: RoomInfo.self)
|
||||
static let fetchRoomInfo = ActionTemplate(id: ".fetchRoomInfo", payloadType: String.self)
|
||||
static let attendeesPerFetch = 20
|
||||
|
||||
// MARK: - private property.
|
||||
private lazy var store: Store<ConferenceInfo, ServiceCenter> = {
|
||||
let store = Store.init(initialState: ConferenceInfo(), environment: ServiceCenter(), reducers: [self.conferenceReducer])
|
||||
store.register(effects: scheduleConferenceEffects())
|
||||
return store
|
||||
}()
|
||||
|
||||
private let conferenceReducer = Reducer<ConferenceInfo>(
|
||||
ReduceOn(updateConferenceInfo, reduce: { state, action in
|
||||
state = action.payload
|
||||
}),
|
||||
ReduceOn(updateAttendeeList, reduce: { state, action in
|
||||
state.attendeeListResult.attendeeList.append(contentsOf: action.payload.0)
|
||||
state.attendeeListResult.fetchCursor = action.payload.1
|
||||
state.attendeeListResult.totalCount = action.payload.2
|
||||
}),
|
||||
ReduceOn(updateBasicInfo, reduce: { state, action in
|
||||
state.basicInfo = action.payload
|
||||
})
|
||||
)
|
||||
|
||||
deinit {
|
||||
store.unregister(reducer: conferenceReducer)
|
||||
store.unregisterEffects(withId: scheduleConferenceEffects.id)
|
||||
}
|
||||
}
|
||||
|
||||
extension ScheduleConferenceStoreProvider: ScheduleConferenceStore {
|
||||
func fetchAttendees(cursor: String) {
|
||||
let conferenceId = conferenceInfo.basicInfo.roomId
|
||||
store.dispatch(action: ScheduleConferenceStoreProvider.fetchAttendeeList(payload: (conferenceId, cursor, ScheduleConferenceStoreProvider.attendeesPerFetch)))
|
||||
}
|
||||
|
||||
func update(conference info: ConferenceInfo) {
|
||||
store.dispatch(action: ScheduleConferenceStoreProvider.updateConferenceInfo(payload: info))
|
||||
}
|
||||
|
||||
func fetchRoomInfo(roomId: String) {
|
||||
store.dispatch(action: ScheduleConferenceStoreProvider.fetchRoomInfo(payload: roomId))
|
||||
}
|
||||
|
||||
func select<Value>(_ selector: Selector<ConferenceInfo, Value>) -> AnyPublisher<Value, Never> where Value : Equatable {
|
||||
return store.select(selector)
|
||||
}
|
||||
|
||||
var conferenceInfo: ConferenceInfo {
|
||||
return store.state
|
||||
}
|
||||
}
|
||||
|
||||
class scheduleConferenceEffects: Effects {
|
||||
typealias Environment = ServiceCenter
|
||||
|
||||
let fetchAttendeeList = Effect<Environment>.dispatchingOne { actions, environment in
|
||||
actions.wasCreated(from: ScheduleConferenceStoreProvider.fetchAttendeeList)
|
||||
.flatMap { action in
|
||||
environment.conferenceListService.fetchAttendeeList(conferenceId: action.payload.0,
|
||||
cursor: action.payload.1,
|
||||
count: action.payload.2)
|
||||
.map { (userInfoList, cursor, totalCount) in
|
||||
ScheduleConferenceStoreProvider.updateAttendeeList(payload: (userInfoList, cursor, totalCount))
|
||||
}
|
||||
.catch { error -> Just<Action> in
|
||||
Just(ErrorActions.throwError(payload: error))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let fetchRoomInfo = Effect<Environment>.dispatchingOne { actions, environment in
|
||||
actions.wasCreated(from: ScheduleConferenceStoreProvider.fetchRoomInfo)
|
||||
.flatMap { action in
|
||||
environment.conferenceListService.fetchConferenceInfo(roomId: action.payload)
|
||||
.map { conferenceInfo in
|
||||
ScheduleConferenceStoreProvider.updateBasicInfo(payload: conferenceInfo.basicInfo)
|
||||
}
|
||||
.catch { error -> Just<Action> in
|
||||
Just(ErrorActions.throwError(payload: error))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
//
|
||||
// ScheduleConferenceDataHelper.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RTCRoomEngine
|
||||
|
||||
class ScheduleConferenceDataHelper {
|
||||
open class func generateScheduleConferenceData(route: Route,
|
||||
store: ScheduleConferenceStore,
|
||||
operation: ConferenceStore,
|
||||
viewController: ContactViewSelectDelegate? = nil) -> [Int: [CellConfigItem]] {
|
||||
var menus: [Int:[CellConfigItem]] = [:]
|
||||
menus[0] = getFirstSectionMenus(route: route, store: store, viewController: viewController)
|
||||
menus[1] = getSecondSectionMenus(route: route, store: store)
|
||||
menus[2] = getThirdSectionMenus(route: route, store: store)
|
||||
menus[3] = getFourthSectionMenus(route: route, store: store, operation: operation)
|
||||
return menus
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - private function.
|
||||
extension ScheduleConferenceDataHelper {
|
||||
|
||||
class func getFirstSectionMenus(route: Route, store: ScheduleConferenceStore, viewController: ContactViewSelectDelegate?) -> [CellConfigItem] {
|
||||
var array: [CellConfigItem] = []
|
||||
array.append(getConferenceNameItem(route: route, store: store))
|
||||
array.append(getConferenceTypeItem(route: route, store: store))
|
||||
array.append(getStartTimeItem(route: route, store: store))
|
||||
array.append(getDurationTimeItem(route: route, store: store))
|
||||
array.append(getTimeZoneItem(route: route, store: store))
|
||||
array.append(getParticipatingMembersItem(route: route, store: store, viewController: viewController))
|
||||
return array
|
||||
}
|
||||
|
||||
class func getSecondSectionMenus(route: Route, store: ScheduleConferenceStore) -> [CellConfigItem] {
|
||||
var array: [CellConfigItem] = []
|
||||
array.append(getEncryptRoomItem(store: store))
|
||||
if store.conferenceInfo.basicInfo.isPasswordEnabled {
|
||||
array.append(getRoomPasswordItem(store: store))
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
class func getThirdSectionMenus(route: Route, store: ScheduleConferenceStore) -> [CellConfigItem] {
|
||||
var array: [CellConfigItem] = []
|
||||
array.append(getMuteAllItem(route: route, store: store))
|
||||
array.append(getFreezeVideoItem(route: route, store: store))
|
||||
return array
|
||||
}
|
||||
|
||||
class func getFourthSectionMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> [CellConfigItem] {
|
||||
return [getbookItem(route: route, store: store, operation: operation)]
|
||||
}
|
||||
|
||||
class func getConferenceNameItem(route: Route, store: ScheduleConferenceStore) -> TextFieldItem{
|
||||
var conferenceNameItem = TextFieldItem(title: .roomNameText, content: store.conferenceInfo.basicInfo.name)
|
||||
conferenceNameItem.saveTextClosure = { text in
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.basicInfo.name = text
|
||||
store.update(conference: conferenceInfo)
|
||||
}
|
||||
conferenceNameItem.bindStateClosure = { cell, cancellableSet in
|
||||
let getBasicInfo = Selector(keyPath: \ConferenceInfo.basicInfo)
|
||||
let selector = Selector.with(getBasicInfo, keyPath: \RoomInfo.name)
|
||||
store.select(selector)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak cell] text in
|
||||
if let cell = cell as? TextFieldCell {
|
||||
cell.textField.text = text
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
return conferenceNameItem
|
||||
}
|
||||
|
||||
class func getConferenceTypeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
let enableSeatControl = store.conferenceInfo.basicInfo.isSeatEnabled
|
||||
var conferenceTypeItem = ListItem(title: .roomTypeText, content: enableSeatControl ? .onStageSpeechRoomText: .freeSpeechRoomText)
|
||||
conferenceTypeItem.showButton = true
|
||||
conferenceTypeItem.selectClosure = {
|
||||
let view = RoomTypeView()
|
||||
view.dismissAction = {
|
||||
route.dismiss(animated: true)
|
||||
}
|
||||
route.present(route: .popup(view: view))
|
||||
}
|
||||
conferenceTypeItem.bindStateClosure = { cell, cancellableSet in
|
||||
let getBasicInfo = Selector(keyPath: \ConferenceInfo.basicInfo)
|
||||
let selector = Selector.with(getBasicInfo, keyPath: \RoomInfo.isSeatEnabled)
|
||||
store.select(selector)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak cell] enableSeatControl in
|
||||
if let cell = cell as? ScheduleTabCell {
|
||||
cell.messageLabel.text = enableSeatControl ? .onStageSpeechRoomText: .freeSpeechRoomText
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
return conferenceTypeItem
|
||||
}
|
||||
|
||||
class func getStartTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
let startTime = TimeInterval(store.conferenceInfo.scheduleStartTime)
|
||||
var startTimeItem = ListItem(title: .startingTimeText, content: getTimeIntervalString(startTime, timeZone: store.conferenceInfo.timeZone))
|
||||
startTimeItem.showButton = true
|
||||
startTimeItem.selectClosure = {
|
||||
let view = TimePickerView()
|
||||
view.pickerDate = Date(timeIntervalSince1970: TimeInterval(store.conferenceInfo.scheduleStartTime))
|
||||
view.dismissAction = {
|
||||
route.dismiss(animated: true)
|
||||
}
|
||||
route.present(route: .popup(view: view))
|
||||
}
|
||||
startTimeItem.bindStateClosure = { cell, cancellableSet in
|
||||
let selector = Selector(keyPath: \ConferenceInfo.scheduleStartTime)
|
||||
store.select(selector)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak cell] startTime in
|
||||
if let cell = cell as? ScheduleTabCell {
|
||||
cell.messageLabel.text = getTimeIntervalString(TimeInterval(startTime), timeZone: store.conferenceInfo.timeZone)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
return startTimeItem
|
||||
}
|
||||
|
||||
class func getDurationTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
var durationTimeItem = ListItem(title: .roomDurationText, content: getDurationTimeString(store.conferenceInfo.durationTime))
|
||||
durationTimeItem.showButton = true
|
||||
durationTimeItem.selectClosure = {
|
||||
let view = DurationPickerView()
|
||||
view.dismissAction = {
|
||||
route.dismiss(animated: true)
|
||||
}
|
||||
route.present(route: .popup(view: view))
|
||||
}
|
||||
durationTimeItem.bindStateClosure = { cell, cancellableSet in
|
||||
let selector = Selector(keyPath: \ConferenceInfo.durationTime)
|
||||
store.select(selector)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { durationTime in
|
||||
if let cell = cell as? ScheduleTabCell {
|
||||
cell.messageLabel.text = getDurationTimeString(durationTime)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
return durationTimeItem
|
||||
}
|
||||
|
||||
class func getTimeZoneItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
var timeZoneItem = ListItem(title: .rimeZoneText, content: store.conferenceInfo.timeZone.getTimeZoneName())
|
||||
timeZoneItem.showButton = true
|
||||
timeZoneItem.buttonIcon = "room_right_arrow1"
|
||||
timeZoneItem.selectClosure = {
|
||||
route.pushTo(route: .timeZone)
|
||||
}
|
||||
timeZoneItem.bindStateClosure = { cell, cancellableSet in
|
||||
let selector = Selector(keyPath: \ConferenceInfo.timeZone)
|
||||
store.select(selector)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { timeZone in
|
||||
if let cell = cell as? ScheduleTabCell {
|
||||
cell.messageLabel.text = timeZone.getTimeZoneName()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
return timeZoneItem
|
||||
}
|
||||
|
||||
class func getParticipatingMembersItem(route: Route, store: ScheduleConferenceStore, viewController: ContactViewSelectDelegate? = nil) -> ListItem {
|
||||
var participatingMembersItem = ListItem(title: .participatingMembersText)
|
||||
participatingMembersItem.showButton = true
|
||||
participatingMembersItem.buttonIcon = "room_right_arrow1"
|
||||
participatingMembersItem.selectClosure = { [weak viewController] in
|
||||
guard let vc = viewController else { return }
|
||||
let users = store.conferenceInfo.attendeeListResult.attendeeList.map { $0.convertToUser() }
|
||||
let participants = ConferenceParticipants(selectedList: users)
|
||||
route.showContactView(delegate: vc, participants: participants)
|
||||
}
|
||||
participatingMembersItem.bindStateClosure = { cell, cancellableSet in
|
||||
let selector = Selector(keyPath: \ConferenceInfo.attendeeListResult.attendeeList)
|
||||
store.select(selector)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { list in
|
||||
if let cell = cell as? ScheduleTabCell {
|
||||
var iconList: [String] = []
|
||||
for i in 0...2 {
|
||||
if let userInfo = list[safe: i] {
|
||||
let avatarUrl = userInfo.avatarUrl.count > 0 ? userInfo.avatarUrl : "room_default_avatar_rect"
|
||||
iconList.append(avatarUrl)
|
||||
}
|
||||
}
|
||||
cell.updateStackView(iconList: iconList)
|
||||
let totalCount = store.conferenceInfo.attendeeListResult.totalCount
|
||||
cell.messageLabel.text = totalCount == 0 ? .addToText : localizedReplace(.participantsNumber, replace: String(totalCount))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
return participatingMembersItem
|
||||
}
|
||||
|
||||
class func getEncryptRoomItem(store: ScheduleConferenceStore) -> SwitchItem {
|
||||
var encryptRoomItem = SwitchItem(title: .encryptTheRoomText)
|
||||
encryptRoomItem.isOn = store.conferenceInfo.basicInfo.isPasswordEnabled
|
||||
encryptRoomItem.selectClosure = {
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.basicInfo.isPasswordEnabled = !store.conferenceInfo.basicInfo.isPasswordEnabled
|
||||
store.update(conference: conferenceInfo)
|
||||
}
|
||||
return encryptRoomItem
|
||||
}
|
||||
|
||||
class func getRoomPasswordItem(store: ScheduleConferenceStore) -> TextFieldItem {
|
||||
var roomPasswordItem = TextFieldItem(title: .roomPasswordText, content: store.conferenceInfo.basicInfo.password)
|
||||
roomPasswordItem.keyboardType = .numberPad
|
||||
roomPasswordItem.maxLengthInBytes = 6
|
||||
roomPasswordItem.placeholder = .enterJoinRoomPassword
|
||||
roomPasswordItem.saveTextClosure = { text in
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.basicInfo.password = text
|
||||
store.update(conference: conferenceInfo)
|
||||
}
|
||||
return roomPasswordItem
|
||||
}
|
||||
|
||||
class func getMuteAllItem(route: Route, store: ScheduleConferenceStore) -> SwitchItem {
|
||||
var muteAllItem = SwitchItem(title: .muteAllText)
|
||||
muteAllItem.isOn = store.conferenceInfo.basicInfo.isMicrophoneDisableForAllUser
|
||||
muteAllItem.selectClosure = {
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.basicInfo.isMicrophoneDisableForAllUser = !conferenceInfo.basicInfo.isMicrophoneDisableForAllUser
|
||||
store.update(conference: conferenceInfo)
|
||||
}
|
||||
muteAllItem.bindStateClosure = { cell, cancellableSet in
|
||||
let selector = Selector(keyPath: \ConferenceInfo.basicInfo.isMicrophoneDisableForAllUser)
|
||||
store.select(selector)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { isMicrophoneDisableForAllUser in
|
||||
if let cell = cell as? SwitchCell {
|
||||
cell.rightSwitch.isOn = isMicrophoneDisableForAllUser
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
return muteAllItem
|
||||
}
|
||||
|
||||
class func getFreezeVideoItem(route: Route, store: ScheduleConferenceStore) -> SwitchItem {
|
||||
var freezeVideoItem = SwitchItem(title: .freezeVideoText)
|
||||
freezeVideoItem.isOn = store.conferenceInfo.basicInfo.isCameraDisableForAllUser
|
||||
freezeVideoItem.selectClosure = {
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.basicInfo.isCameraDisableForAllUser = !conferenceInfo.basicInfo.isCameraDisableForAllUser
|
||||
store.update(conference: conferenceInfo)
|
||||
}
|
||||
freezeVideoItem.bindStateClosure = { cell, cancellableSet in
|
||||
let selector = Selector(keyPath: \ConferenceInfo.basicInfo.isCameraDisableForAllUser)
|
||||
store.select(selector)
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { isCameraDisableForAllUser in
|
||||
if let cell = cell as? SwitchCell {
|
||||
cell.rightSwitch.isOn = isCameraDisableForAllUser
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
return freezeVideoItem
|
||||
}
|
||||
|
||||
class func getbookItem(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> ButtonItem {
|
||||
var bookItem = ButtonItem(title: .bookRoomText)
|
||||
bookItem.titleColor = UIColor(0xFFFFFF)
|
||||
bookItem.backgroudColor = UIColor(0x1C66E5)
|
||||
bookItem.selectClosure = {
|
||||
guard TimeInterval(store.conferenceInfo.scheduleStartTime) >= Date().timeIntervalSince1970 else {
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .startTimeCannotEarlierCurrentTime)))
|
||||
return
|
||||
}
|
||||
guard store.conferenceInfo.basicInfo.name.count > 0 else {
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .nameCannotBeEmptyText)))
|
||||
return
|
||||
}
|
||||
guard checkPasswordFormat(conferenceInfo: store.conferenceInfo) else {
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .passwordFormatIsIncorrect)))
|
||||
return
|
||||
}
|
||||
let conferenceInfo = TUIConferenceInfo(conferenceInfo: store.conferenceInfo)
|
||||
operation.dispatch(action: ConferenceListActions.scheduleConference(payload: conferenceInfo))
|
||||
}
|
||||
return bookItem
|
||||
}
|
||||
|
||||
class func checkPasswordFormat(conferenceInfo: ConferenceInfo) -> Bool {
|
||||
if conferenceInfo.basicInfo.isPasswordEnabled {
|
||||
let password = conferenceInfo.basicInfo.password
|
||||
let passwordLength = 6
|
||||
return password.count == passwordLength && password.isStringOnlyDigits()
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class func getTimeIntervalString(_ time: TimeInterval, timeZone: TimeZone) -> String {
|
||||
let date = Date(timeIntervalSince1970: time)
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "MM-dd HH:mm"
|
||||
dateFormatter.timeZone = timeZone
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
class func getDurationTimeString(_ time: UInt) -> String {
|
||||
guard time > 0 else { return "" }
|
||||
let hour = time / 3_600
|
||||
let minute = (time / 60) % 60
|
||||
var text = ""
|
||||
if hour > 0 {
|
||||
text = String(hour) + .hour
|
||||
}
|
||||
if minute > 0 {
|
||||
text = text + String(minute) + .minute
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static let roomNameText = localized("Room name")
|
||||
static let roomTypeText = localized("Room type")
|
||||
static let startingTimeText = localized("Starting time")
|
||||
static let roomDurationText = localized("Room duration")
|
||||
static let rimeZoneText = localized("Time zone")
|
||||
static let freeSpeechRoomText = localized("Free Speech Room")
|
||||
static let onStageSpeechRoomText = localized("On-stage Speech Room")
|
||||
static let muteAllText = localized("Mute All")
|
||||
static let freezeVideoText = localized("Freeze video")
|
||||
static let hour = localized("hour")
|
||||
static let minute = localized("minute")
|
||||
static let participatingMembersText = localized("Participating members")
|
||||
static let addToText = localized("Add to")
|
||||
static let encryptTheRoomText = localized("Encrypt the room")
|
||||
static let roomPasswordText = localized("Room Password")
|
||||
static let bookRoomText = localized("Schedule Room")
|
||||
static let nameCannotBeEmptyText = localized("Conference name cannot be empty!")
|
||||
static let participantsNumber = localized("xx/300 people")
|
||||
static let passwordFormatIsIncorrect = localized("Your room password format is incorrect, please check it")
|
||||
static let enterJoinRoomPassword = localized("Enter 6-digit password")
|
||||
static let startTimeCannotEarlierCurrentTime = localized("The start time cannot be earlier than the current time")
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// ScheduleConferenceTableView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
|
||||
class ScheduleConferenceTableView: UIView {
|
||||
var menus: [Int: [CellConfigItem]]
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .grouped)
|
||||
tableView.separatorStyle = .none
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.sectionFooterHeight = 20.scale375Height()
|
||||
tableView.sectionHeaderHeight = 0
|
||||
tableView.register(ScheduleTabCell.self, forCellReuseIdentifier: ScheduleTabCell.identifier)
|
||||
tableView.register(SwitchCell.self, forCellReuseIdentifier: SwitchCell.identifier)
|
||||
tableView.register(TextFieldCell.self, forCellReuseIdentifier: TextFieldCell.identifier)
|
||||
tableView.register(ButtonCell.self, forCellReuseIdentifier: ButtonCell.identifier)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
backgroundColor = UIColor(0xF8F9FB)
|
||||
}
|
||||
|
||||
init(menus: [Int : [CellConfigItem]]) {
|
||||
self.menus = menus
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(tableView)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.bottom.equalToSuperview().offset(-10.scale375Height())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ScheduleConferenceTableView: UITableViewDataSource {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return menus.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
guard let value = menus[section] else { return 0 }
|
||||
return value.count
|
||||
}
|
||||
}
|
||||
|
||||
extension ScheduleConferenceTableView: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let items = menus[indexPath.section] ?? []
|
||||
let item = items[indexPath.row]
|
||||
let identifier = item.cellType.cellIdentifier
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier:identifier, for: indexPath)
|
||||
if let scheduleTabCell = cell as? ScheduleTabCell {
|
||||
scheduleTabCell.updateView(item: item)
|
||||
} else if let switchCell = cell as? SwitchCell {
|
||||
switchCell.updateView(item: item)
|
||||
} else if let textFieldCell = cell as? TextFieldCell {
|
||||
textFieldCell.updateView(item: item)
|
||||
} else if let buttonCell = cell as? ButtonCell {
|
||||
buttonCell.updateView(item: item)
|
||||
}
|
||||
if let baseCell = cell as? ScheduleBaseCell {
|
||||
item.bindStateClosure?(baseCell, &baseCell.cancellableSet)
|
||||
}
|
||||
cell.selectionStyle = .none
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let items = menus[indexPath.section] ?? []
|
||||
let item = items[indexPath.row]
|
||||
let cell = tableView.cellForRow(at: indexPath)
|
||||
if cell is ScheduleTabCell {
|
||||
item.selectClosure?()
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
let normalHeight = 45.scale375Height()
|
||||
if indexPath.section == 0 {
|
||||
return normalHeight
|
||||
}
|
||||
guard let itemArray = menus[indexPath.section], let item = itemArray[safe: indexPath.item] else { return normalHeight }
|
||||
switch item.cellType {
|
||||
case .switcher, .textField:
|
||||
return 54.scale375Height()
|
||||
case .button:
|
||||
return 44.scale375Height()
|
||||
default:
|
||||
return normalHeight
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
let rows = tableView.numberOfRows(inSection: indexPath.section)
|
||||
if indexPath.row == 0 || indexPath.row == rows - 1 {
|
||||
var corner = UIRectCorner()
|
||||
if rows == 1 {
|
||||
corner = .allCorners
|
||||
} else if indexPath.row == 0 {
|
||||
corner = [.topLeft, .topRight]
|
||||
} else if indexPath.row == rows - 1 {
|
||||
corner = [.bottomLeft, .bottomRight]
|
||||
}
|
||||
cell.roundedRect(rect: cell.bounds,
|
||||
byRoundingCorners: corner,
|
||||
cornerRadii: CGSize(width: 12, height: 12))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//
|
||||
// ScheduleDetailsDataHelper.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RTCRoomEngine
|
||||
|
||||
class ScheduleDetailsDataHelper: ScheduleConferenceDataHelper {
|
||||
|
||||
class func generateScheduleDetailsConferenceData(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore, viewStore: ConferenceMainViewStore) -> [Int : [CellConfigItem]] {
|
||||
var menus: [Int:[CellConfigItem]] = [:]
|
||||
menus[0] = getFirstSectionDetailsMenus(route: route, store: store, operation: operation)
|
||||
menus[1] = getSecondSectionDetailsMenus(store: store, operation: operation, viewStore: viewStore)
|
||||
menus[2] = getThirdSectionDetailsMenus(route: route, store: store, operation: operation)
|
||||
guard let fourthSectionDetailsMenus = getFourthSectionDetailsMenus(route: route, store: store, operation: operation) else { return menus }
|
||||
menus[3] = fourthSectionDetailsMenus
|
||||
return menus
|
||||
}
|
||||
|
||||
private class func getFirstSectionDetailsMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> [CellConfigItem] {
|
||||
var array: [CellConfigItem] = []
|
||||
array.append(getDetailsConferenceNameItem(route: route, store: store))
|
||||
array.append(getDetailsConferenceIdItem(route: route, store: store, operation: operation))
|
||||
array.append(getDetailsStartTimeItem(route: route, store: store))
|
||||
array.append(getDetailsDurationTimeItem(route: route, store: store))
|
||||
array.append(getDetailsConferenceTypeItem(route: route, store: store))
|
||||
if let passwordItem = getConferencePasswordItem(store: store) {
|
||||
array.append(passwordItem)
|
||||
}
|
||||
array.append(getRoomHostItem(route: route, store: store))
|
||||
array.append(getDetailsParticipatingMembersItem(route: route, store: store))
|
||||
return array
|
||||
}
|
||||
|
||||
private class func getSecondSectionDetailsMenus(store: ScheduleConferenceStore, operation: ConferenceStore, viewStore: ConferenceMainViewStore) -> [CellConfigItem] {
|
||||
return [getEnterRoomItem(store: store, operation: operation, viewStore: viewStore)]
|
||||
}
|
||||
|
||||
private class func getThirdSectionDetailsMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> [CellConfigItem] {
|
||||
return [getInviteItem(route: route, store: store)]
|
||||
}
|
||||
|
||||
private class func getFourthSectionDetailsMenus(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> [CellConfigItem]? {
|
||||
guard store.conferenceInfo.basicInfo.ownerId == operation.selectCurrent(UserSelectors.getSelfId) else { return nil }
|
||||
guard store.conferenceInfo.status == .notStarted else { return nil }
|
||||
return [getCancelRoomItem(route: route, store: store, operation: operation)]
|
||||
}
|
||||
|
||||
private class func getDetailsConferenceNameItem(route: Route, store: ScheduleConferenceStore) -> TextFieldItem {
|
||||
var conferenceNameItem = getConferenceNameItem(route: route, store: store)
|
||||
conferenceNameItem.isEnable = false
|
||||
return conferenceNameItem
|
||||
}
|
||||
|
||||
private class func getDetailsConferenceIdItem(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> ListItem {
|
||||
var conferenceIdItem = ListItem(title: .roomIDText, content: store.conferenceInfo.basicInfo.roomId)
|
||||
conferenceIdItem.showButton = true
|
||||
conferenceIdItem.buttonIcon = "room_copy_blue"
|
||||
conferenceIdItem.selectClosure = {
|
||||
UIPasteboard.general.string = store.conferenceInfo.basicInfo.roomId
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .copyRoomIdSuccess)))
|
||||
}
|
||||
return conferenceIdItem
|
||||
}
|
||||
|
||||
private class func getDetailsConferenceTypeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
var conferenceTypeItem = getConferenceTypeItem(route: route, store: store)
|
||||
conferenceTypeItem.showButton = false
|
||||
conferenceTypeItem.selectClosure = nil
|
||||
return conferenceTypeItem
|
||||
}
|
||||
|
||||
private class func getDetailsStartTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
var startTimeItem = getStartTimeItem(route: route, store: store)
|
||||
startTimeItem.selectClosure = nil
|
||||
startTimeItem.showButton = false
|
||||
return startTimeItem
|
||||
}
|
||||
|
||||
private class func getDetailsDurationTimeItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
var durationTimeItem = getDurationTimeItem(route: route, store: store)
|
||||
durationTimeItem.selectClosure = nil
|
||||
durationTimeItem.showButton = false
|
||||
return durationTimeItem
|
||||
}
|
||||
|
||||
private class func getRoomHostItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
var hostItem = ListItem(title: .creatorText, content: store.conferenceInfo.basicInfo.ownerName)
|
||||
hostItem.iconList = [store.conferenceInfo.basicInfo.ownerAvatarUrl]
|
||||
return hostItem
|
||||
}
|
||||
|
||||
private class func getDetailsParticipatingMembersItem(route: Route, store: ScheduleConferenceStore) -> ListItem {
|
||||
var item = getParticipatingMembersItem(route: route, store: store)
|
||||
item.buttonIcon = "room_down_arrow1"
|
||||
item.selectClosure = {
|
||||
if store.conferenceInfo.attendeeListResult.attendeeList.count > 0 {
|
||||
route.present(route: .selectedMember(showDeleteButton: false, selectedMembers: store.conferenceInfo.attendeeListResult.attendeeList))
|
||||
}
|
||||
}
|
||||
item.bindStateClosure = { cell, cancellableSet in
|
||||
let selector = Selector(keyPath: \ConferenceInfo.attendeeListResult.attendeeList)
|
||||
store.select(selector)
|
||||
.receive(on: RunLoop.main)
|
||||
.removeDuplicates()
|
||||
.sink { list in
|
||||
if let cell = cell as? ScheduleTabCell {
|
||||
var iconList: [String] = []
|
||||
for i in 0...2 {
|
||||
if let userInfo = list[safe: i] {
|
||||
let avatarUrl = userInfo.avatarUrl.count > 0 ? userInfo.avatarUrl : "room_default_avatar_rect"
|
||||
iconList.append(avatarUrl)
|
||||
}
|
||||
}
|
||||
cell.updateStackView(iconList: iconList)
|
||||
let totalCount = store.conferenceInfo.attendeeListResult.totalCount
|
||||
cell.messageLabel.text = totalCount == 0 ? .noParticipantsYet : localizedReplace(.participantsNumber, replace: String(totalCount))
|
||||
cell.updateButton(isShown: list.count > 0)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
private class func getConferencePasswordItem(store: ScheduleConferenceStore) -> ListItem? {
|
||||
guard store.conferenceInfo.basicInfo.password.count > 0 else { return nil }
|
||||
var passwordItem = ListItem(title: .conferencePasswordText)
|
||||
passwordItem.content = store.conferenceInfo.basicInfo.password
|
||||
passwordItem.selectClosure = nil
|
||||
passwordItem.showButton = false
|
||||
return passwordItem
|
||||
}
|
||||
|
||||
private class func getEnterRoomItem(store: ScheduleConferenceStore, operation: ConferenceStore, viewStore: ConferenceMainViewStore) -> ButtonItem {
|
||||
var item = ButtonItem(title: .enterTheRoomText)
|
||||
item.titleColor = UIColor(0x0961F7)
|
||||
item.backgroudColor = UIColor(0xF0F3FA)
|
||||
item.selectClosure = {
|
||||
let conferenceId = store.conferenceInfo.basicInfo.roomId
|
||||
operation.dispatch(action: RoomActions.joinConference(payload: conferenceId))
|
||||
operation.dispatch(action: ScheduleViewActions.popDetailView())
|
||||
viewStore.updateInternalCreation(isInternalCreation: true)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
private class func getInviteItem(route: Route, store: ScheduleConferenceStore) -> ButtonItem {
|
||||
var item = ButtonItem(title: .inviteMemberText)
|
||||
item.titleColor = UIColor(0x0961F7)
|
||||
item.backgroudColor = UIColor(0xF0F3FA)
|
||||
item.selectClosure = {
|
||||
let view = InviteEnterRoomView(conferenceInfo: store.conferenceInfo)
|
||||
route.present(route: .popup(view: view))
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
private class func getCancelRoomItem(route: Route, store: ScheduleConferenceStore, operation: ConferenceStore) -> ButtonItem {
|
||||
var item = ButtonItem(title: .cancelRoomText)
|
||||
item.titleColor = UIColor(0xED414D)
|
||||
item.backgroudColor = UIColor(0xFAF0F0)
|
||||
item.selectClosure = {
|
||||
let declineAction = UIAlertAction(title: .notCanceledYet, style: .cancel)
|
||||
declineAction.setValue(UIColor(0x4F586B), forKey: "titleTextColor")
|
||||
let sureAction = UIAlertAction(title: .cancelRoom, style: .default) { _ in
|
||||
operation.dispatch(action: ConferenceListActions.cancelConference(payload: store.conferenceInfo.basicInfo.roomId))
|
||||
}
|
||||
sureAction.setValue(UIColor(0xED414D), forKey: "titleTextColor")
|
||||
let alertState = AlertState(title: .cancelBookedRoomTitle, message: .cancelBookedRoomMessage, sureAction: sureAction, declineAction: declineAction)
|
||||
route.present(route: .alert(state: alertState))
|
||||
}
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static let creatorText: String = localized("Creator")
|
||||
static let roomIDText: String = localized("Room ID")
|
||||
static let enterTheRoomText = localized("Enter the room")
|
||||
static let inviteMemberText = localized("Invite member")
|
||||
static let cancelRoomText = localized("Cancel Room")
|
||||
static let cancelBookedRoomTitle = localized("Cancel this booked room")
|
||||
static let cancelBookedRoomMessage = localized("After cancellation, other members will not be able to join")
|
||||
static let notCanceledYet = localized("Not canceled yet")
|
||||
static let cancelRoom = localized("Cancel Room")
|
||||
static let copyRoomIdSuccess = localized("Conference ID copied.")
|
||||
static let noParticipantsYet = localized("No participants yet")
|
||||
static let participantsNumber = localized("xx/300 people")
|
||||
static let conferencePasswordText = localized("Conference password")
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
//
|
||||
// ScheduleDetailsViewController .swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
import Combine
|
||||
import TUICore
|
||||
import RTCRoomEngine
|
||||
|
||||
class ScheduleDetailsViewController: UIViewController {
|
||||
var conferenceInfo: ConferenceInfo
|
||||
private var cancellableSet = Set<AnyCancellable>()
|
||||
|
||||
private lazy var rootView: ScheduleConferenceTableView = {
|
||||
return ScheduleConferenceTableView(menus: ScheduleDetailsDataHelper.generateScheduleDetailsConferenceData(route: route, store: store, operation: operation, viewStore: viewStore))
|
||||
}()
|
||||
|
||||
private lazy var conferenceListPublisher = {
|
||||
operation.select(ConferenceListSelectors.getConferenceList)
|
||||
}()
|
||||
|
||||
init(conferenceInfo: ConferenceInfo) {
|
||||
self.conferenceInfo = conferenceInfo
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
store.update(conference: self.conferenceInfo)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
self.view = rootView
|
||||
}
|
||||
|
||||
lazy var modifyButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(.reviseText, for: .normal)
|
||||
button.setTitleColor(UIColor(0x1C66E5), for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||
button.addTarget(self, action: #selector(modifyAction(sender:)), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
initState()
|
||||
subscribeToast()
|
||||
subscribeScheduleSubject()
|
||||
navigationItem.title = .roomDetailsText
|
||||
if store.conferenceInfo.basicInfo.ownerId == operation.selectCurrent(UserSelectors.getSelfId),
|
||||
store.conferenceInfo.status == .notStarted {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: modifyButton)
|
||||
}
|
||||
store.fetchAttendees(cursor: "")
|
||||
let cursorSelector = Selector(keyPath: \ConferenceInfo.attendeeListResult.fetchCursor)
|
||||
store.select(cursorSelector)
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink {[weak self] cursor in
|
||||
guard let self = self else { return }
|
||||
if !cursor.isEmpty {
|
||||
self.store.fetchAttendees(cursor: cursor)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
|
||||
let selector = Selector(keyPath: \ConferenceInfo.basicInfo.isPasswordEnabled)
|
||||
store.select(selector)
|
||||
.removeDuplicates()
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] isPasswordEnabled in
|
||||
guard let self = self else { return }
|
||||
self.rootView.menus = ScheduleDetailsDataHelper.generateScheduleDetailsConferenceData(route: self.route, store: self.store, operation: self.operation, viewStore: self.viewStore)
|
||||
self.rootView.tableView.reloadData()
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
|
||||
operation.select(ViewSelectors.getPopDetailFlag)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] shouldPopDetail in
|
||||
guard let self = self else { return }
|
||||
if shouldPopDetail {
|
||||
self.route.pop(route: .scheduleDetails(conferenceInfo:self.conferenceInfo))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
|
||||
conferenceListPublisher
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] list in
|
||||
guard let self = self else { return }
|
||||
guard let selectedConferenceInfo = list.first(where: { $0.basicInfo.roomId == self.conferenceInfo.basicInfo.roomId }) else { return }
|
||||
guard selectedConferenceInfo.status != self.store.conferenceInfo.status, selectedConferenceInfo.status == .running else { return }
|
||||
self.modifyButton.isHidden = true
|
||||
var conferenceInfo = self.store.conferenceInfo
|
||||
conferenceInfo.status = selectedConferenceInfo.status
|
||||
self.store.update(conference: conferenceInfo)
|
||||
let menus = ScheduleDetailsDataHelper.generateScheduleDetailsConferenceData(route: self.route, store: self.store, operation: self.operation, viewStore: self.viewStore)
|
||||
self.rootView.menus = menus
|
||||
self.rootView.tableView.reloadData()
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
store.fetchRoomInfo(roomId: conferenceInfo.basicInfo.roomId)
|
||||
}
|
||||
|
||||
private func initState() {
|
||||
self.operation.dispatch(action: UserActions.getSelfInfo())
|
||||
self.operation.dispatch(action: ScheduleViewActions.resetPopDetailFlag())
|
||||
|
||||
}
|
||||
|
||||
@objc func modifyAction(sender: UIButton) {
|
||||
route.pushTo(route: .modifySchedule(conferenceInfo: store.conferenceInfo))
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
|
||||
// MARK: - private property.
|
||||
@Injected(\.navigation) private var route
|
||||
@Injected(\.scheduleStore) private var store
|
||||
@Injected(\.conferenceStore) private var operation
|
||||
@Injected(\.conferenceMainViewStore) private var viewStore
|
||||
}
|
||||
|
||||
extension ScheduleDetailsViewController {
|
||||
private func subscribeToast() {
|
||||
operation.toastSubject
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] toast in
|
||||
guard let self = self else { return }
|
||||
var position = TUICSToastPositionBottom
|
||||
switch toast.position {
|
||||
case .center:
|
||||
position = TUICSToastPositionCenter
|
||||
default:
|
||||
break
|
||||
}
|
||||
if self.presentedViewController == nil {
|
||||
self.view.makeToast(toast.message, duration: toast.duration, position: position)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
|
||||
private func subscribeScheduleSubject() {
|
||||
operation.scheduleActionSubject
|
||||
.receive(on: RunLoop.main)
|
||||
.filter { $0.id == ScheduleResponseActions.onCancelSuccess.id }
|
||||
.sink { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.route.pop()
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static let roomDetailsText = localized("Room Details")
|
||||
static let reviseText = localized("Revise")
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// ButtonCell.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/7/5.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ButtonCell: ScheduleBaseCell {
|
||||
static let identifier = "ButtonCell"
|
||||
var item: CellConfigItem?
|
||||
|
||||
let button: UIButton = {
|
||||
let button = UIButton()
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||
button.titleLabel?.textAlignment = .center
|
||||
button.layer.cornerRadius = 12
|
||||
return button
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(button)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
button.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
button.addTarget(self, action: #selector(buttonAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func buttonAction(sender: UIButton) {
|
||||
item?.selectClosure?()
|
||||
}
|
||||
|
||||
func updateView(item: CellConfigItem) {
|
||||
self.item = item
|
||||
guard let buttonItem = item as? ButtonItem else { return }
|
||||
button.setTitle(buttonItem.title, for: .normal)
|
||||
button.setTitleColor(buttonItem.titleColor, for: .normal)
|
||||
button.backgroundColor = buttonItem.backgroudColor
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// DurationPickerView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
|
||||
class DurationPickerView: UIView {
|
||||
var dismissAction: (() -> Void)?
|
||||
var timeDuration: TimeInterval = 1800
|
||||
|
||||
let topView: UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let topLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .roomDurationText
|
||||
label.font = UIFont.systemFont(ofSize: 16, weight: .heavy)
|
||||
label.textColor = UIColor(0x22262E)
|
||||
return label
|
||||
}()
|
||||
|
||||
let cancelButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "schedule_wrong", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
let sureButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "schedule_right", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
let timePickerView: UIDatePicker = {
|
||||
let pickerView = UIDatePicker()
|
||||
pickerView.datePickerMode = .countDownTimer
|
||||
if #available(iOS 13.4, *) {
|
||||
pickerView.preferredDatePickerStyle = .wheels
|
||||
}
|
||||
pickerView.minuteInterval = 5
|
||||
return pickerView
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
backgroundColor = UIColor(0xFFFFFF)
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(topView)
|
||||
topView.addSubview(topLabel)
|
||||
topView.addSubview(cancelButton)
|
||||
topView.addSubview(sureButton)
|
||||
addSubview(timePickerView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
topView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.bottom.equalTo(timePickerView.snp.top)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(54.scale375Height())
|
||||
}
|
||||
topLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(24.scale375())
|
||||
make.leading.equalToSuperview().offset(20.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
sureButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-20.scale375())
|
||||
make.width.height.equalTo(cancelButton)
|
||||
make.centerY.equalTo(cancelButton)
|
||||
}
|
||||
timePickerView.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(270.scale375Height())
|
||||
make.bottom.equalToSuperview().offset(-5.scale375Height())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
self.layer.cornerRadius = 12
|
||||
timePickerView.countDownDuration = timeDuration
|
||||
timePickerView.addTarget(self, action: #selector(datePickerValueChanged(_:)), for: .valueChanged)
|
||||
sureButton.addTarget(self, action: #selector(sureAction(sender:)), for: .touchUpInside)
|
||||
cancelButton.addTarget(self, action: #selector(cancelAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func sureAction(sender: UIButton) {
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.durationTime = UInt(timePickerView.countDownDuration)
|
||||
store.update(conference: conferenceInfo)
|
||||
dismissAction?()
|
||||
}
|
||||
|
||||
@objc func cancelAction(sender: UIButton) {
|
||||
dismissAction?()
|
||||
}
|
||||
|
||||
@objc func datePickerValueChanged(_ sender: UIDatePicker) {
|
||||
let selectedTimeInterval = sender.countDownDuration
|
||||
let minTimeInterval = TimeInterval(900)
|
||||
if selectedTimeInterval < minTimeInterval {
|
||||
sender.countDownDuration = minTimeInterval
|
||||
}
|
||||
}
|
||||
|
||||
deinit{
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
|
||||
@Injected(\.modifyScheduleStore) var store: ScheduleConferenceStore
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var roomDurationText: String {
|
||||
localized("Room duration")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// InviteEnterRoomDataHelper.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/7/5.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
|
||||
class InviteEnterRoomDataHelper {
|
||||
class func generateInviteEnterRoomHelperData(conferenceInfo: ConferenceInfo, operation: ConferenceStore) -> [ListCellItemData] {
|
||||
var array: [ListCellItemData] = []
|
||||
array.append(getRoomNameItem(conferenceInfo: conferenceInfo))
|
||||
array.append(getRoomTypeItem(conferenceInfo: conferenceInfo))
|
||||
array.append(getRoomDurationItem(conferenceInfo: conferenceInfo))
|
||||
array.append(getRoomIdItem(conferenceInfo: conferenceInfo, operation: operation))
|
||||
if let passwordItem = getRoomPasswordItem(conferenceInfo: conferenceInfo, operation: operation) {
|
||||
array.append(passwordItem)
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
private class func getRoomNameItem(conferenceInfo: ConferenceInfo) -> ListCellItemData {
|
||||
let roomNameItem = getListCellItem(title: .roomName, message: conferenceInfo.basicInfo.name, hasRightButton: false)
|
||||
return roomNameItem
|
||||
}
|
||||
|
||||
private class func getRoomTypeItem(conferenceInfo: ConferenceInfo) -> ListCellItemData {
|
||||
let message: String = conferenceInfo.basicInfo.isSeatEnabled ? .onStageSpeechRoom : .freeSpeechRoom
|
||||
let roomNameItem = getListCellItem(title: .roomType, message: message, hasRightButton: false)
|
||||
return roomNameItem
|
||||
}
|
||||
|
||||
private class func getRoomDurationItem(conferenceInfo: ConferenceInfo) -> ListCellItemData {
|
||||
let scheduleStartTime = getTimeIntervalString(TimeInterval(conferenceInfo.scheduleStartTime), dateFormat: "MM-dd HH:mm")
|
||||
let scheduleEndTime = getTimeIntervalString(TimeInterval(conferenceInfo.scheduleEndTime), dateFormat: "HH:mm")
|
||||
let nextDayText = isTimeInNextDay(conferenceInfo: conferenceInfo) ? .nextDay : ""
|
||||
let message = scheduleStartTime + "-" + nextDayText + scheduleEndTime
|
||||
let roomNameItem = getListCellItem(title: .roomDuration, message: message, hasRightButton: false)
|
||||
return roomNameItem
|
||||
}
|
||||
|
||||
private class func isTimeInNextDay(conferenceInfo: ConferenceInfo) -> Bool {
|
||||
let startDate = Date(timeIntervalSince1970: TimeInterval(conferenceInfo.scheduleStartTime))
|
||||
let endDate = Date(timeIntervalSince1970: TimeInterval(conferenceInfo.scheduleEndTime))
|
||||
let calendar = Calendar.current
|
||||
let startDay = calendar.dateComponents([.year, .month, .day], from: startDate).day ?? 0
|
||||
let endDay = calendar.dateComponents([.year, .month, .day], from: endDate).day ?? 0
|
||||
return endDay - startDay == 1
|
||||
}
|
||||
|
||||
private class func getTimeIntervalString(_ time: TimeInterval, dateFormat: String) -> String {
|
||||
let date = Date(timeIntervalSince1970: time)
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = dateFormat
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
private class func getRoomIdItem(conferenceInfo: ConferenceInfo, operation: ConferenceStore) -> ListCellItemData {
|
||||
let roomIdItem = getListCellItem(title: .roomIdText, message: conferenceInfo.basicInfo.roomId, hasRightButton: true)
|
||||
roomIdItem.buttonData?.action = { _ in
|
||||
UIPasteboard.general.string = conferenceInfo.basicInfo.roomId
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .copyRoomIdSuccess)))
|
||||
}
|
||||
return roomIdItem
|
||||
}
|
||||
|
||||
private class func getRoomPasswordItem(conferenceInfo: ConferenceInfo, operation: ConferenceStore) -> ListCellItemData? {
|
||||
let password = conferenceInfo.basicInfo.password
|
||||
guard password.count > 0 else { return nil }
|
||||
let passwordItem = getListCellItem(title: .conferencePasswordText, message: password, hasRightButton: true)
|
||||
passwordItem.buttonData?.action = { _ in
|
||||
UIPasteboard.general.string = password
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .conferencePasswordSuccess)))
|
||||
}
|
||||
return passwordItem
|
||||
}
|
||||
|
||||
private class func getRoomLinkItem(roomId: String, operation: ConferenceStore) -> ListCellItemData? {
|
||||
guard let roomLink = getRoomLink(roomId: roomId) else { return nil }
|
||||
let roomLinkItem = getListCellItem(title: .roomLinkText, message: roomLink, hasRightButton: true)
|
||||
roomLinkItem.buttonData?.action = { _ in
|
||||
UIPasteboard.general.string = roomLink
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .copyRoomLinkSuccess)))
|
||||
}
|
||||
return roomLinkItem
|
||||
}
|
||||
|
||||
private class func getListCellItem(title: String, message: String, hasRightButton: Bool) -> ListCellItemData {
|
||||
let item = ListCellItemData()
|
||||
item.titleText = title
|
||||
item.messageText = message
|
||||
item.hasRightButton = hasRightButton
|
||||
if hasRightButton {
|
||||
item.buttonData = getCopyButtonItem()
|
||||
}
|
||||
item.titleColor = UIColor(0x8F9AB2)
|
||||
item.messageColor = UIColor(0x4F586B)
|
||||
item.backgroundColor = .clear
|
||||
return item
|
||||
}
|
||||
|
||||
private class func getCopyButtonItem() -> ButtonItemData {
|
||||
let buttonData = ButtonItemData()
|
||||
buttonData.normalIcon = "room_copy"
|
||||
buttonData.normalTitle = .copyText
|
||||
buttonData.cornerRadius = 4
|
||||
buttonData.titleFont = UIFont(name: "PingFangSC-Regular", size: 12)
|
||||
buttonData.titleColor = UIColor(0x4F586B)
|
||||
buttonData.backgroundColor = UIColor(0xD5E0F2).withAlphaComponent(0.7)
|
||||
buttonData.resourceBundle = tuiRoomKitBundle()
|
||||
return buttonData
|
||||
}
|
||||
|
||||
private class func getRoomLink(roomId: String) -> String? {
|
||||
guard let bundleId = Bundle.main.bundleIdentifier else { return nil }
|
||||
if bundleId == "com.tencent.tuiroom.apiexample" || bundleId == "com.tencent.fx.rtmpdemo" {
|
||||
return "https://web.sdk.qcloud.com/trtc/webrtc/test/tuiroom-inner/index.html#/" + "room?roomId=" + roomId
|
||||
} else if bundleId == "com.tencent.mrtc" {
|
||||
return "https://web.sdk.qcloud.com/component/tuiroom/index.html#/" + "room?roomId=" + roomId
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@Injected(\.conferenceStore) private var operation
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var roomIdText: String {
|
||||
localized("Room ID")
|
||||
}
|
||||
static var roomLinkText: String {
|
||||
localized("Room link")
|
||||
}
|
||||
static var copyText: String {
|
||||
localized("Copy")
|
||||
}
|
||||
static var copyRoomIdSuccess: String {
|
||||
localized("Conference ID copied.")
|
||||
}
|
||||
static var copyRoomLinkSuccess: String {
|
||||
localized("Conference Link copied.")
|
||||
}
|
||||
static let conferencePasswordText = localized("Conference password")
|
||||
static let conferencePasswordSuccess = localized("Conference password copied successfully.")
|
||||
static let roomName = localized("Room name")
|
||||
static let roomType = localized("Room type")
|
||||
static let roomDuration = localized("Room duration")
|
||||
static let freeSpeechRoom = localized("Free Speech Room")
|
||||
static let onStageSpeechRoom = localized("On-stage Speech Room")
|
||||
static let nextDay = localized("Next Day")
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//
|
||||
// InviteEnterRoomView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/7/5.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
import TUICore
|
||||
import Combine
|
||||
|
||||
enum InviteEnterRoomViewStyle {
|
||||
case normal
|
||||
case inviteWhenSuccess
|
||||
}
|
||||
|
||||
class InviteEnterRoomView: UIView {
|
||||
private var cancellableSet = Set<AnyCancellable>()
|
||||
var style: InviteEnterRoomViewStyle = .normal
|
||||
var title: String {
|
||||
if self.style == .normal {
|
||||
return .inviteMember
|
||||
} else {
|
||||
return .inviteWhenSuccess
|
||||
}
|
||||
}
|
||||
let conferenceInfo: ConferenceInfo
|
||||
lazy var menus = {
|
||||
InviteEnterRoomDataHelper.generateInviteEnterRoomHelperData(conferenceInfo: conferenceInfo, operation: operation)
|
||||
}()
|
||||
|
||||
private let dropArrowButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "room_drop_arrow", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 10.scale375Height(), left: 20.scale375(), bottom: 20.scale375Height(), right: 20.scale375())
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.textColor = UIColor(0x4F586B)
|
||||
label.font = UIFont.systemFont(ofSize: 18, weight: .medium)
|
||||
label.textAlignment = isRTL ? .right : .left
|
||||
return label
|
||||
}()
|
||||
|
||||
private let stackView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .vertical
|
||||
view.alignment = .center
|
||||
view.distribution = .equalSpacing
|
||||
view.spacing = 10
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
private let copyButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(.copyRoomInformation, for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||
button.setTitleColor(UIColor(0x1C66E5), for: .normal)
|
||||
button.titleLabel?.textAlignment = .center
|
||||
button.layer.cornerRadius = 6
|
||||
button.layer.borderWidth = 1
|
||||
button.layer.borderColor = UIColor(0x1C66E5).cgColor
|
||||
return button
|
||||
}()
|
||||
|
||||
init(conferenceInfo: ConferenceInfo, style: InviteEnterRoomViewStyle = .normal) {
|
||||
self.conferenceInfo = conferenceInfo
|
||||
self.style = style
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
backgroundColor = UIColor(0xFFFFFF)
|
||||
layer.cornerRadius = 14
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
addSubview(dropArrowButton)
|
||||
addSubview(titleLabel)
|
||||
addSubview(stackView)
|
||||
addSubview(copyButton)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
dropArrowButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(dropArrowButton.snp.bottom)
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
}
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(20.scale375Height())
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
}
|
||||
copyButton.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16.scale375())
|
||||
make.trailing.equalToSuperview().offset(-16.scale375())
|
||||
make.height.equalTo(44.scale375Height())
|
||||
make.top.equalTo(stackView.snp.bottom).offset(20.scale375Height())
|
||||
make.bottom.equalToSuperview().offset(-34.scale375Height())
|
||||
}
|
||||
|
||||
for item in menus {
|
||||
let view = ListCellItemView(itemData: item)
|
||||
stackView.addArrangedSubview(view)
|
||||
view.snp.makeConstraints { make in
|
||||
make.height.equalTo(20.scale375Height())
|
||||
make.width.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
subscribeToast()
|
||||
dropArrowButton.addTarget(self, action: #selector(dropAction(sender: )), for: .touchUpInside)
|
||||
copyButton.addTarget(self, action: #selector(copyAction(sender: )), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func dropAction(sender: UIButton) {
|
||||
route.dismiss(animated: true)
|
||||
}
|
||||
|
||||
@objc func copyAction(sender: UIButton) {
|
||||
var conferenceDetails = title
|
||||
menus.forEach { item in
|
||||
conferenceDetails = conferenceDetails + "\n\(item.titleText) : \(item.messageText)"
|
||||
}
|
||||
UIPasteboard.general.string = conferenceDetails
|
||||
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .roomInformationCopiedSuccessfully)))
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
|
||||
@Injected(\.navigation) private var route
|
||||
@Injected(\.conferenceStore) private var operation
|
||||
}
|
||||
|
||||
|
||||
extension InviteEnterRoomView {
|
||||
private func subscribeToast() {
|
||||
operation.toastSubject
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] toast in
|
||||
guard let self = self else { return }
|
||||
var position = TUICSToastPositionBottom
|
||||
switch toast.position {
|
||||
case .center:
|
||||
position = TUICSToastPositionCenter
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.makeToast(toast.message, duration: toast.duration, position: position)
|
||||
}
|
||||
.store(in: &cancellableSet)
|
||||
}}
|
||||
|
||||
private extension String {
|
||||
static let inviteMember = localized("Invite members to join")
|
||||
static let inviteWhenSuccess = localized("Booking successful, invite members to join")
|
||||
static let copyRoomInformation = localized("Copy room information")
|
||||
static let roomInformationCopiedSuccessfully = localized("Room information copied successfully")
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// RoomTypeView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Factory
|
||||
|
||||
class RoomTypeView: UIView {
|
||||
var dismissAction: (() -> Void)?
|
||||
|
||||
let freedomButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(.freedomSpeakText, for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||
button.setTitleColor(UIColor(0x22262E), for: .normal)
|
||||
button.backgroundColor = UIColor(0xFFFFFF)
|
||||
return button
|
||||
}()
|
||||
|
||||
let raiseHandButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(.raiseHandSpeakText, for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||
button.setTitleColor(UIColor(0x22262E), for: .normal)
|
||||
button.backgroundColor = UIColor(0xFFFFFF)
|
||||
return button
|
||||
}()
|
||||
|
||||
let cancelButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setTitle(.cancelText, for: .normal)
|
||||
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
button.setTitleColor(UIColor(0x22262E), for: .normal)
|
||||
button.backgroundColor = UIColor(0xFFFFFF)
|
||||
return button
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(freedomButton)
|
||||
addSubview(raiseHandButton)
|
||||
addSubview(cancelButton)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
freedomButton.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.bottom.equalTo(raiseHandButton.snp.top)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(58.scale375Height())
|
||||
}
|
||||
raiseHandButton.snp.makeConstraints { make in
|
||||
make.bottom.equalTo(cancelButton.snp.top)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(58.scale375Height())
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.bottom.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(97.scale375Height())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
layer.cornerRadius = 12
|
||||
freedomButton.addTarget(self, action: #selector(freedomAction(sender:)), for: .touchUpInside)
|
||||
raiseHandButton.addTarget(self, action: #selector(raiseHandAction(sender:)), for: .touchUpInside)
|
||||
cancelButton.addTarget(self, action: #selector(cancelAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func freedomAction(sender: UIButton) {
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.basicInfo.isSeatEnabled = false
|
||||
store.update(conference: conferenceInfo)
|
||||
dismissAction?()
|
||||
}
|
||||
|
||||
@objc func raiseHandAction(sender: UIButton) {
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.basicInfo.isSeatEnabled = true
|
||||
store.update(conference: conferenceInfo)
|
||||
dismissAction?()
|
||||
}
|
||||
|
||||
@objc func cancelAction(sender: UIButton) {
|
||||
dismissAction?()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
|
||||
@Injected(\.modifyScheduleStore) var store: ScheduleConferenceStore
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var raiseHandSpeakText: String {
|
||||
localized("On-stage Speech Room")
|
||||
}
|
||||
static var freedomSpeakText: String {
|
||||
localized("Free Speech Room")
|
||||
}
|
||||
static var cancelText: String {
|
||||
localized("Cancel")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// ScheduleBaseCell.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by aby on 2024/6/28.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
class ScheduleBaseCell: UITableViewCell {
|
||||
var cancellableSet = Set<AnyCancellable>()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
cancellableSet.forEach { cancelable in
|
||||
cancelable.cancel()
|
||||
}
|
||||
cancellableSet.removeAll()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// CellConfigItem.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
enum ScheduleConfigCellType {
|
||||
case list
|
||||
case switcher
|
||||
case textField
|
||||
case button
|
||||
|
||||
var cellIdentifier: String {
|
||||
switch self {
|
||||
case .list:
|
||||
return ScheduleTabCell.identifier
|
||||
case .switcher:
|
||||
return SwitchCell.identifier
|
||||
case .textField:
|
||||
return TextFieldCell.identifier
|
||||
case .button:
|
||||
return ButtonCell.identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias CellSelectClosure = ()->Void
|
||||
typealias CellStateBinderClosure = (UITableViewCell, inout Set<AnyCancellable>)->Void
|
||||
|
||||
protocol CellConfigItem {
|
||||
var cellType: ScheduleConfigCellType { get }
|
||||
var title: String { get }
|
||||
var selectClosure: CellSelectClosure? { get set }
|
||||
var bindStateClosure: CellStateBinderClosure? { get set }
|
||||
var isEnable: Bool { get set }
|
||||
}
|
||||
|
||||
struct ListItem: CellConfigItem {
|
||||
var cellType: ScheduleConfigCellType = .list
|
||||
var title: String
|
||||
var content: String = ""
|
||||
var isEnable: Bool = true
|
||||
|
||||
var showButton: Bool = false
|
||||
var buttonIcon: String = "room_down_arrow1"
|
||||
|
||||
var selectClosure: CellSelectClosure?
|
||||
var bindStateClosure: CellStateBinderClosure?
|
||||
|
||||
var iconList: [String] = []
|
||||
}
|
||||
|
||||
struct SwitchItem: CellConfigItem {
|
||||
var cellType: ScheduleConfigCellType = .switcher
|
||||
var title: String
|
||||
var isOn: Bool = true
|
||||
var isEnable: Bool = true
|
||||
|
||||
var selectClosure: CellSelectClosure?
|
||||
var bindStateClosure: CellStateBinderClosure?
|
||||
}
|
||||
|
||||
struct TextFieldItem: CellConfigItem {
|
||||
var cellType: ScheduleConfigCellType = .textField
|
||||
var title: String
|
||||
var selectClosure: CellSelectClosure?
|
||||
var bindStateClosure: CellStateBinderClosure?
|
||||
var saveTextClosure: ((String) -> Void)?
|
||||
var isEnable: Bool = true
|
||||
var content: String = ""
|
||||
var keyboardType: UIKeyboardType = .default
|
||||
var maxLengthInBytes: Int = 100
|
||||
var placeholder: String = ""
|
||||
}
|
||||
|
||||
struct ButtonItem: CellConfigItem {
|
||||
var cellType: ScheduleConfigCellType = .button
|
||||
var title: String
|
||||
var selectClosure: CellSelectClosure?
|
||||
var bindStateClosure: CellStateBinderClosure?
|
||||
var isEnable: Bool = true
|
||||
var titleColor: UIColor?
|
||||
var backgroudColor: UIColor?
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// ScheduleTabCell.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/5.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class ScheduleTabCell: ScheduleBaseCell {
|
||||
static let identifier = "ScheduleTabCell"
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let view = UILabel()
|
||||
view.backgroundColor = .clear
|
||||
view.textColor = UIColor(0x2B2E38)
|
||||
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
view.textAlignment = isRTL ? .right : .left
|
||||
return view
|
||||
}()
|
||||
|
||||
let messageLabel: UILabel = {
|
||||
let view = UILabel()
|
||||
view.backgroundColor = .clear
|
||||
view.textColor = UIColor(0x2B2E38).withAlphaComponent(0.7)
|
||||
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
view.textAlignment = isRTL ? .left : .right
|
||||
return view
|
||||
}()
|
||||
|
||||
let button: UIButton = {
|
||||
let button = UIButton()
|
||||
let image = UIImage(named: "room_down_arrow1", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
button.setImage(image, for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
let avatarsView: UIStackView = {
|
||||
let view = UIStackView()
|
||||
view.axis = .horizontal
|
||||
view.alignment = isRTL ? .leading : .trailing
|
||||
view.spacing = 5
|
||||
return view
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
setupViewStyle()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(button)
|
||||
contentView.addSubview(messageLabel)
|
||||
contentView.addSubview(avatarsView)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(20.scale375())
|
||||
make.width.lessThanOrEqualTo(100.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
button.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-20.scale375())
|
||||
make.width.height.equalTo(16.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
messageLabel.snp.makeConstraints() { make in
|
||||
make.trailing.equalTo(button.snp.leading).offset(-5.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.lessThanOrEqualTo(170.scale375())
|
||||
}
|
||||
avatarsView.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(messageLabel.snp.leading).offset(-5.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.lessThanOrEqualTo(120.scale375())
|
||||
}
|
||||
}
|
||||
|
||||
private func setupViewStyle() {
|
||||
// TODO: - @janejntang use color theme define from design graph.
|
||||
backgroundColor = UIColor(0xFFFFFF)
|
||||
}
|
||||
|
||||
func updateView(item: CellConfigItem) {
|
||||
guard let listItem = item as? ListItem else { return }
|
||||
titleLabel.text = listItem.title
|
||||
messageLabel.text = listItem.content
|
||||
button.setImage(UIImage(named: listItem.buttonIcon, in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
updateButton(isShown: listItem.showButton)
|
||||
updateStackView(iconList: listItem.iconList)
|
||||
}
|
||||
|
||||
func updateStackView(iconList: [String]) {
|
||||
avatarsView.arrangedSubviews.forEach { view in
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
let placeHolderImage = UIImage(named: "room_default_avatar_rect", in: tuiRoomKitBundle(), compatibleWith: nil)
|
||||
for iconString in iconList {
|
||||
let imageView = UIImageView(image: placeHolderImage)
|
||||
if let url = URL(string: iconString ) {
|
||||
imageView.sd_setImage(with: url, placeholderImage: placeHolderImage)
|
||||
}
|
||||
imageView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(32.scale375())
|
||||
}
|
||||
avatarsView.addArrangedSubview(imageView)
|
||||
}
|
||||
titleLabel.snp.updateConstraints { make in
|
||||
let maxWidth = iconList.count > 0 ? 80.scale375() : 100.scale375()
|
||||
make.width.lessThanOrEqualTo(maxWidth)
|
||||
}
|
||||
}
|
||||
|
||||
func updateButton(isShown: Bool) {
|
||||
let buttonWidth = isShown ? 16.scale375() : 0
|
||||
button.snp.updateConstraints { make in
|
||||
make.width.equalTo(buttonWidth)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// SwitchCell.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/13.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class SwitchCell: ScheduleBaseCell {
|
||||
static let identifier = "SwitchCell"
|
||||
var item: CellConfigItem?
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let view = UILabel()
|
||||
view.backgroundColor = .clear
|
||||
view.textColor = UIColor(0x2B2E38)
|
||||
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
view.textAlignment = isRTL ? .right : .left
|
||||
return view
|
||||
}()
|
||||
|
||||
let rightSwitch: UISwitch = {
|
||||
let view = UISwitch()
|
||||
view.isOn = true
|
||||
view.onTintColor = UIColor(0x0062E3)
|
||||
return view
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(rightSwitch)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(20.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
rightSwitch.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-20.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func bindInteraction() {
|
||||
rightSwitch.addTarget(self, action: #selector(switchAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func switchAction(sender: UISwitch) {
|
||||
item?.selectClosure?()
|
||||
}
|
||||
|
||||
func updateView(item: CellConfigItem) {
|
||||
guard let switchItem = item as? SwitchItem else { return }
|
||||
self.item = item
|
||||
titleLabel.text = switchItem.title
|
||||
rightSwitch.isOn = switchItem.isOn
|
||||
rightSwitch.isEnabled = switchItem.isEnable
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// TextFieldCell.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/7/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
|
||||
class TextFieldCell: ScheduleBaseCell {
|
||||
static let identifier = "TextFieldCell"
|
||||
private var item: CellConfigItem?
|
||||
|
||||
let titleLabel: UILabel = {
|
||||
let view = UILabel()
|
||||
view.backgroundColor = .clear
|
||||
view.textColor = UIColor(0x2B2E38)
|
||||
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
view.textAlignment = isRTL ? .right : .left
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var textField: UITextField = {
|
||||
let view = UITextField(frame: .zero)
|
||||
view.backgroundColor = .clear
|
||||
view.textColor = UIColor(0x2B2E38).withAlphaComponent(0.7)
|
||||
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
view.keyboardType = .default
|
||||
view.textAlignment = isRTL ? .left : .right
|
||||
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100.scale375(), height: 50.scale375Height()))
|
||||
toolbar.barStyle = .default
|
||||
toolbar.items = [UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil), UIBarButtonItem(title: .ok, style: .done, target: self, action: #selector(saveTextField))]
|
||||
view.inputAccessoryView = toolbar
|
||||
view.delegate = self
|
||||
return view
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
guard !isViewReady else { return }
|
||||
isViewReady = true
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
}
|
||||
|
||||
private func constructViewHierarchy() {
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(textField)
|
||||
}
|
||||
|
||||
private func activateConstraints() {
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(20.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.lessThanOrEqualTo(100.scale375())
|
||||
}
|
||||
textField.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-23.scale375())
|
||||
make.leading.equalTo(titleLabel.snp.trailing).offset(20.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func updateView(item: CellConfigItem) {
|
||||
guard let textFieldItem = item as? TextFieldItem else { return }
|
||||
self.item = item
|
||||
titleLabel.text = item.title
|
||||
textField.text = textFieldItem.content
|
||||
textField.isEnabled = textFieldItem.isEnable
|
||||
textField.keyboardType = textFieldItem.keyboardType
|
||||
textField.placeholder = textFieldItem.placeholder
|
||||
}
|
||||
|
||||
@objc func saveTextField() {
|
||||
if let textFieldItem = item as? TextFieldItem, let text = textField.text {
|
||||
textFieldItem.saveTextClosure?(text)
|
||||
}
|
||||
textField.resignFirstResponder()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension TextFieldCell: UITextFieldDelegate {
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
guard let textFieldItem = item as? TextFieldItem else { return false }
|
||||
guard let text = textField.text else { return true }
|
||||
if string.count > 0, string.trimmingCharacters(in: .whitespaces).isEmpty, range.location == 0 {
|
||||
return false
|
||||
}
|
||||
let currentLengthInBytes = Array(text.utf8).count
|
||||
let replacementLengthInBytes = Array(string.utf8).count
|
||||
if currentLengthInBytes + replacementLengthInBytes > textFieldItem.maxLengthInBytes {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static let ok = localized("OK")
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// TimePickerView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
|
||||
class TimePickerView: UIView {
|
||||
var dismissAction: (() -> Void)?
|
||||
var pickerDate: Date?
|
||||
|
||||
let topView: UIView = {
|
||||
let view = UIView()
|
||||
return view
|
||||
}()
|
||||
|
||||
let topLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .startingTimeText
|
||||
label.font = UIFont.systemFont(ofSize: 16, weight: .heavy)
|
||||
label.textColor = UIColor(0x22262E)
|
||||
return label
|
||||
}()
|
||||
|
||||
let cancelButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "schedule_wrong", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
let sureButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "schedule_right", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
return button
|
||||
}()
|
||||
|
||||
lazy var timePickerView: UIDatePicker = {
|
||||
let pickerView = UIDatePicker()
|
||||
pickerView.datePickerMode = .dateAndTime
|
||||
if #available(iOS 14.0, *) {
|
||||
pickerView.preferredDatePickerStyle = .wheels
|
||||
}
|
||||
pickerView.minuteInterval = 5
|
||||
pickerView.timeZone = store.conferenceInfo.timeZone
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MM-dd HH:mm"
|
||||
var minimumTime = Date().timeIntervalSince1970
|
||||
let remainder = minimumTime.remainder(dividingBy: 300)
|
||||
if remainder > 60 || remainder < 0 {
|
||||
minimumTime = Date().addingTimeInterval(150).timeIntervalSince1970
|
||||
minimumTime = minimumTime - minimumTime.remainder(dividingBy: 300)
|
||||
}
|
||||
if let pickerDate = pickerDate {
|
||||
pickerView.date = pickerDate
|
||||
} else {
|
||||
pickerView.date = Date(timeIntervalSince1970: minimumTime)
|
||||
}
|
||||
pickerView.minimumDate = Date(timeIntervalSince1970: minimumTime)
|
||||
return pickerView
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
backgroundColor = UIColor(0xFFFFFF)
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(topView)
|
||||
topView.addSubview(topLabel)
|
||||
topView.addSubview(cancelButton)
|
||||
topView.addSubview(sureButton)
|
||||
addSubview(timePickerView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
topView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.bottom.equalTo(timePickerView.snp.top)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(54.scale375Height())
|
||||
}
|
||||
topLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(24.scale375())
|
||||
make.leading.equalToSuperview().offset(20.scale375())
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
sureButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-20.scale375())
|
||||
make.width.height.equalTo(cancelButton)
|
||||
make.centerY.equalTo(cancelButton)
|
||||
}
|
||||
timePickerView.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(300.scale375Height())
|
||||
make.bottom.equalToSuperview().offset(-5.scale375Height())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
self.layer.cornerRadius = 12
|
||||
sureButton.addTarget(self, action: #selector(sureAction(sender:)), for: .touchUpInside)
|
||||
cancelButton.addTarget(self, action: #selector(cancelAction(sender:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func sureAction(sender: UIButton) {
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.scheduleStartTime = UInt(timePickerView.date.timeIntervalSince1970)
|
||||
store.update(conference: conferenceInfo)
|
||||
dismissAction?()
|
||||
}
|
||||
|
||||
@objc func cancelAction(sender: UIButton) {
|
||||
dismissAction?()
|
||||
}
|
||||
|
||||
deinit{
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
|
||||
@Injected(\.modifyScheduleStore) var store: ScheduleConferenceStore
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var startingTimeText: String {
|
||||
localized("Starting time")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
//
|
||||
// TimeZoneView.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Factory
|
||||
|
||||
class TimeZoneView: UIView {
|
||||
lazy var menus = TimeZone.knownTimeZoneIdentifiers.sorted { id1, id2 in
|
||||
let timeZone1 = TimeZone(identifier: id1)?.secondsFromGMT() ?? 0
|
||||
let timeZone2 = TimeZone(identifier: id2)?.secondsFromGMT() ?? 0
|
||||
return timeZone1 < timeZone2
|
||||
}
|
||||
|
||||
var selectedTimeZone: String? {
|
||||
didSet {
|
||||
self.updateSelected()
|
||||
}
|
||||
}
|
||||
|
||||
let backButton: UIButton = {
|
||||
let button = UIButton()
|
||||
button.setImage(UIImage(named: "room_back_black", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
|
||||
button.contentEdgeInsets = UIEdgeInsets(top: 10.scale375Height(), left: 20.scale375(), bottom: 10.scale375Height(), right: 20.scale375())
|
||||
return button
|
||||
}()
|
||||
|
||||
let topLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = .selectTimeZoneText
|
||||
label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||
label.textColor = UIColor(0x2B2E38)
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
tableView.separatorStyle = .none
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.backgroundColor = UIColor(0x17181F)
|
||||
tableView.register(TimeZoneCell.self, forCellReuseIdentifier: TimeZoneCell.identifier)
|
||||
tableView.backgroundColor = UIColor(0xFFFFFF)
|
||||
return tableView
|
||||
}()
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
backgroundColor = UIColor(0xFFFFFF)
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
bindInteraction()
|
||||
isViewReady = true
|
||||
selectedTimeZone = self.store.conferenceInfo.timeZone.identifier
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(backButton)
|
||||
addSubview(topLabel)
|
||||
addSubview(tableView)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
backButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(safeAreaLayoutGuide.snp.top)
|
||||
make.leading.equalToSuperview()
|
||||
}
|
||||
topLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(backButton)
|
||||
make.centerX.equalToSuperview()
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(backButton.snp.bottom).offset(10.scale375Height())
|
||||
make.leading.equalToSuperview().offset(20.scale375())
|
||||
make.trailing.equalToSuperview().offset(-20.scale375())
|
||||
make.bottom.equalToSuperview().offset(-20.scale375Height())
|
||||
}
|
||||
}
|
||||
|
||||
func bindInteraction() {
|
||||
backButton.addTarget(self, action: #selector(backAction(sender: )), for: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc func backAction(sender: UIButton) {
|
||||
route.pop()
|
||||
}
|
||||
|
||||
private func updateSelected() {
|
||||
if let selectedTimeZone = selectedTimeZone,
|
||||
let index = menus.firstIndex(of: selectedTimeZone) {
|
||||
let indexPath = IndexPath(row: index, section: 0)
|
||||
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .top)
|
||||
}
|
||||
}
|
||||
|
||||
deinit{
|
||||
debugPrint("deinit:\(self)")
|
||||
}
|
||||
|
||||
@Injected(\.navigation) private var route
|
||||
@Injected(\.modifyScheduleStore) var store: ScheduleConferenceStore
|
||||
}
|
||||
|
||||
extension TimeZoneView: UITableViewDataSource {
|
||||
internal func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return menus.count
|
||||
}
|
||||
}
|
||||
|
||||
extension TimeZoneView: UITableViewDelegate {
|
||||
internal func tableView(_ tableView: UITableView,
|
||||
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TimeZoneCell.identifier, for: indexPath)
|
||||
if let timeZoneCell = cell as? TimeZoneCell, let timeZone = TimeZone(identifier: menus[indexPath.row]) {
|
||||
timeZoneCell.title = timeZone.getTimeZoneName()
|
||||
|
||||
if selectedTimeZone == menus[indexPath.row] {
|
||||
cell.accessoryType = .checkmark
|
||||
} else {
|
||||
cell.accessoryType = .none
|
||||
}
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
internal func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if let timeZone = TimeZone(identifier: menus[indexPath.row]) {
|
||||
var conferenceInfo = store.conferenceInfo
|
||||
conferenceInfo.timeZone = timeZone
|
||||
conferenceInfo.scheduleStartTime = UInt(conferenceInfo.scheduleStartTime)
|
||||
store.update(conference: conferenceInfo)
|
||||
}
|
||||
route.pop()
|
||||
}
|
||||
|
||||
internal func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return 50.scale375Height()
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
static var roomDurationText: String {
|
||||
localized("Room duration")
|
||||
}
|
||||
static let selectTimeZoneText: String = localized("Select time zone")
|
||||
}
|
||||
|
||||
class TimeZoneCell: UITableViewCell {
|
||||
static let identifier = "TimeZoneCell"
|
||||
var title: String = "" {
|
||||
didSet {
|
||||
label.text = title
|
||||
}
|
||||
}
|
||||
let label: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFont.systemFont(ofSize: 16, weight: .regular)
|
||||
label.textColor = UIColor(0x22262E)
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .default, reuseIdentifier: TimeZoneCell.identifier)
|
||||
self.selectionStyle = .none
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var isViewReady: Bool = false
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
backgroundColor = UIColor(0xFFFFFF)
|
||||
guard !isViewReady else { return }
|
||||
constructViewHierarchy()
|
||||
activateConstraints()
|
||||
isViewReady = true
|
||||
}
|
||||
|
||||
func constructViewHierarchy() {
|
||||
addSubview(label)
|
||||
}
|
||||
|
||||
func activateConstraints() {
|
||||
label.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// TimeZoneViewController.swift
|
||||
// TUIRoomKit
|
||||
//
|
||||
// Created by janejntang on 2024/6/27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TimeZoneViewController: UIViewController {
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
self.view = TimeZoneView()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(self)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user