Commit 71141684 authored by Steven杜宇's avatar Steven杜宇

Merge branch 'develop-tx-618' into 'develop'

618分支合并到develop

See merge request !13
parents 68d68b6f 7284dfed
# Quick Run of TUIRoomKit Demo for iOS
_[中文](README.md) | English_
This document describes how to quickly run the TUIRoomKit demo project to make a high-quality audio/video call. For more information on the TUIRoomKit component connection process, see **[Integrating TUIRoomKit (iOS)](https://cloud.tencent.com/document/product/647/84237)**.
## Directory Structure
```
TUIRoomKit
├─ Example // multi-person video conferencing demo project
├─ App // Folder of entering/creating multi-person video conferencing UI code and used images and internationalization string resources
├─ Debug // Folder of the key business code required for project debugging and running
├─ Login // Folder of the login UI and business logic code
└─ TXReplayKit_Screen // Folder of sharing screen
├─ TUIRoomKit // Folder of multi-person video conferencing UI code and used images and internationalization string resources
├─ TUIBarrage // Barrage components
└─ TUIBeauty // Beauty components
```
## Environment Requirements
- Xcode 12.0 or above
-
## Running the Demo
[](id:ui.step1)
### Step 1. Create a TRTC application
1. Go to the [Application management](https://console.cloud.tencent.com/trtc/app) page in the TRTC console, select **Create Application**, enter an application name such as `TUIKitDemo`, and click **Confirm**.
2. Click **Application Information** on the right of the application as shown below:
<img src="https://qcloudimg.tencent-cloud.cn/raw/62f58d310dde3de2d765e9a460b8676a.png" width="900">
3. On the application information page, note down the `SDKAppID` and key as shown below:
<img src="https://qcloudimg.tencent-cloud.cn/raw/bea06852e22a33c77cb41d287cac25db.png" width="900">
>! This feature uses two basic PaaS services of Tencent Cloud: [TRTC](https://www.tencentcloud.com/document/product/647/35078) and [IM](https://www.tencentcloud.com/document/product/1047/33513). When you activate TRTC, IM will be activated automatically. IM is a value-added service.
[](id:ui.step2)
### Step 2. Configure the project
1. Open the demo project `DemoApp.xcworkspace` with Xcode 12.0 or later.
2. Find the `iOS/Example/Debug/GenerateTestUserSig.swift` file in the project.
3. Set the following parameters in `GenerateTestUserSig.swift`:
<ul style="margin:0"><li/>SDKAPPID: `0` by default. Set it to the actual `SDKAppID`.
<li/>SECRETKEY: Left empty by default. Set it to the actual key.</ul>
![](https://qcloudimg.tencent-cloud.cn/raw/1c4eb799c7e06aa2da54ece87ccf993e.png)
[](id:ui.step3)
### Step 3. Compile and run the application
1. Open Terminal, enter the project directory, run the `pod install` command, and wait for it to complete.
2. Open the demo project `TUIRoomKit/Example/DemoApp.xcworkspace` with Xcode 12.0 or later and click **Run**.
[](id:ui.step4)
## Have any questions?
Welcome to join our Telegram Group to communicate with our professional engineers! We are more than happy to hear from you~
Click to join: https://t.me/+EPk6TMZEZMM5OGY1
Or scan the QR code
<img src="https://qcloudimg.tencent-cloud.cn/raw/9c67ed5746575e256b81ce5a60216c5a.jpg" width="320"/>
# TUIRoomKit iOS 示例工程快速跑通
_中文 | [English](README.en.md)_
本文档主要介绍如何快速跑通TUIRoomKit 示例工程,体验高质量多人视频会议,更详细的TUIRoomKit组件接入流程,请点击腾讯云官网文档: [**TUIRoomKit 组件 iOS 接入说明** ](https://cloud.tencent.com/document/product/647/84237)...
![](https://qcloudimg.tencent-cloud.cn/raw/b847f8497287077db8909503e6880e19.svg)
## 目录结构
```
TUIRoomKit
├─ Example // 多人视频会议Demo工程
├─ App // 进入/创建多人视频会议UI代码以及用到的图片及国际化字符串资源文件夹
├─ Debug // 工程调试运行所需的关键业务代码文件夹
├─ Login // 登录UI及业务逻辑代码文件夹
└─ TXReplayKit_Screen // 共享屏幕逻辑代码文件夹
├─ TUIRoomKit // 多人视频会议主要UI代码以及所需的图片、国际化字符串资源文件夹
├─ TUIBarrage // 弹幕组件
└─ TUIBeauty // 美颜组件
```
## 环境准备
iOS 12.0及更高。
## 运行并体验 App
[](id:ui.step1)
### 第一步:创建TRTC的应用
TUIRoomKit 是基于腾讯云 [即时通信 IM](https://cloud.tencent.com/document/product/269/42440)[实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 两项付费 PaaS 服务构建出的音视频通信组件。您可以按照如下步骤开通相关的服务:
1. 登录到 [即时通信 IM 控制台](https://console.cloud.tencent.com/im),单击**创建新应用**,在弹出的对话框中输入您的应用名称,并单击**确定**
![](https://qcloudimg.tencent-cloud.cn/raw/07fa9407da05b76b3dbbd9d2c4714cc8.png)
2. 单击刚刚创建出的应用,进入基本配置页面,并在页面的右下角找到开通腾讯实时音视频服务功能区,单击立即开通即可开通实时音视频TRTC 的 7 天免费试用服务。如果需要正式应用上线,可以前往[**实时音视频控制台**](https://console.cloud.tencent.com/trtc/app)付费购买正式版本。
![](https://qcloudimg.tencent-cloud.cn/raw/daa624cbc9c87c787f2afc5b37a8f272.png)
> **友情提示**:
> 单击 **免费体验** 以后,部分之前使用过 [实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 服务的用户会提示:
> ```java
> [-100013]:TRTC service is suspended. Please check if the package balance is 0 or the Tencent Cloud accountis in arrears
> ```
> 因为新的 IM 音视频通话能力是整合了腾讯云 [实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 和 [即时通信 IM](https://cloud.tencent.com/document/product/269/42440) 两个基础的 PaaS 服务,所以当 [实时音视频 TRTC](https://cloud.tencent.com/document/product/647/16788) 的免费额度(10000分钟)已经过期或者耗尽,就会导致开通此项服务失败,这里您可以单击 [TRTC 控制台](https://console.cloud.tencent.com/trtc/app),找到对应 SDKAppID 的应用管理页,示例如图,开通后付费功能后,再次 **启用应用** 即可正常体验音视频通话能力。
![](https://qcloudimg.tencent-cloud.cn/raw/559f87a883348cf27cf6ac202f769243.png)
3. 进入应用信息后,按下图操作,记录SDKAppID和密钥:
![](https://qcloudimg.tencent-cloud.cn/raw/ca696884bd53233447b22c730ed82205.png)
[](id:ui.step2)
### 第二步:配置工程
1. 使用Xcode(12.0及以上)打开源码工程`DemoApp.xcworkspace`
2. 工程内找到 `iOS/Example/Debug/GenerateTestUserSig.swift` 文件。
3. 设置 `GenerateTestUserSig.swift` 文件中的相关参数:
<ul style="margin:0"><li/>SDKAPPID:默认为0,请设置为实际的 SDKAppID。
<li/>SECRETKEY:默认为空字符串,请设置为实际的密钥信息。</ul>
![](https://qcloudimg.tencent-cloud.cn/raw/1c4eb799c7e06aa2da54ece87ccf993e.png)
[](id:ui.step3)
### 第三步:编译运行
1. 打开Terminal(终端)进入到工程目录下执行`pod install`指令,等待完成。
2. Xcode(12.0及以上的版本)打开源码工程 `TUIRoomKit/iOS/Example/DemoApp.xcworkspace`,单击 **运行** 即可开始调试本 App。
[](id:ui.step4)
>? 如果您在使用过程中,有什么建议或者意见,欢迎您加入我们的 TUIKit 组件交流群 QQ 群:592465424,进行技术交流和产品沟通。
{
"images" : [
{
"filename" : "mic_bottom.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "mic_bottom@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "mic_bottom@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "room_modify_name.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "room_modify_name@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "room_modify_name@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "room_mute_audio1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "room_mute_audio1@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "room_mute_audio1@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "room_mute_share_screen.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "room_mute_share_screen@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "room_mute_share_screen@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "room_unmute_share_screen.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "room_unmute_share_screen@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "room_unmute_share_screen@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
......@@ -14,7 +14,7 @@ class InviteToJoinRoomManager {
let inviteJoinModel = InviteJoinModel(message: message, inviter: inviter)
pushSelectGroupMemberViewController(groupId: message.groupId) { responseData in
guard let modelList =
responseData[TUICore_TUIGroupObjectFactory_SelectGroupMemberVC_ResultUserList] as? [TUIUserModel]
responseData[TUICore_TUIContactObjectFactory_SelectGroupMemberVC_ResultUserList] as? [TUIUserModel]
else { return }
var invitedList: [String] = []
for userModel in modelList {
......@@ -25,15 +25,15 @@ class InviteToJoinRoomManager {
}
class func pushSelectGroupMemberViewController(groupId: String, callback: @escaping TUIValueResultCallback) {
let param = [TUICore_TUIGroupObjectFactory_SelectGroupMemberVC_GroupID: groupId]
let param = [TUICore_TUIContactObjectFactory_SelectGroupMemberVC_GroupID: groupId]
if let navigateController = RoomMessageManager.shared.navigateController {
navigateController.push(TUICore_TUIGroupObjectFactory_SelectGroupMemberVC_Classic, param: param) { responseData in
navigateController.push(TUICore_TUIContactObjectFactory_SelectGroupMemberVC_Classic, param: param) { responseData in
callback(responseData)
}
} else {
let nav = UINavigationController()
let currentViewController = getCurrentWindowViewController()
currentViewController?.present(TUICore_TUIGroupObjectFactory_SelectGroupMemberVC_Classic,
currentViewController?.present(TUICore_TUIContactObjectFactory_SelectGroupMemberVC_Classic,
param: param, embbedIn: nav,
forResult: { responseData in
callback(responseData)
......@@ -47,7 +47,7 @@ class InviteToJoinRoomManager {
guard let dataString = dataDict.convertToString() else { return }
let pushInfo = V2TIMOfflinePushInfo()
invitedList.forEach { userId in
V2TIMManager.sharedInstance().invite(userId,
V2TIMManager.sharedInstance().invite(invitee: userId,
data: dataString,
onlineUserOnly: true,
offlinePushInfo: pushInfo,
......
......@@ -38,24 +38,22 @@ class RoomManager {
engineManager.createRoom(roomInfo: roomInfo) { [weak self] in
guard let self = self else { return }
self.roomObserver.createdRoom()
self.enterRoom(roomId: roomInfo.roomId, isShownConferenceViewController: false)
self.enterRoom(roomId: roomInfo.roomId)
} onError: { _, message in
RoomRouter.makeToast(toast: message)
}
}
func enterRoom(roomId: String, isShownConferenceViewController: Bool = true) {
func enterRoom(roomId: String, onSuccess: TUIRoomInfoBlock? = nil, onError: TUIErrorBlock? = nil) {
roomObserver.registerObserver()
engineManager.store.isImAccess = true
self.roomId = roomId
engineManager.enterRoom(roomId: roomId, enableAudio: engineManager.store.isOpenMicrophone, enableVideo: engineManager.store.isOpenCamera, isSoundOnSpeaker: true) { [weak self] roomInfo in
guard let self = self else { return }
self.roomObserver.enteredRoom()
guard isShownConferenceViewController else { return }
let vc = ConferenceMainViewController()
RoomRouter.shared.push(viewController: vc)
} onError: { _, message in
RoomRouter.makeToast(toast: message)
onSuccess?(roomInfo)
} onError: { code, message in
onError?(code, message)
}
}
......
......@@ -59,12 +59,12 @@ class RoomMessageManager: NSObject {
let messageModel = RoomMessageModel()
messageModel.groupId = self.groupId
messageModel.roomId = roomId
messageModel.ownerName = TUILogin.getNickName() ?? ""
messageModel.ownerName = TUILogin.getNickName() ?? self.userId
messageModel.owner = self.userId
let messageDic = messageModel.getDictFromMessageModel()
guard let jsonString = messageDic.convertToString() else { return }
let jsonData = jsonString.data(using: String.Encoding.utf8)
let message = V2TIMManager.sharedInstance().createCustomMessage(jsonData)
let message = V2TIMManager.sharedInstance().createCustomMessage(data: jsonData)
message?.supportMessageExtension = true
let param = [TUICore_TUIChatService_SendMessageMethod_MsgKey: message]
TUICore.callService(TUICore_TUIChatService, method: TUICore_TUIChatService_SendMessageMethod, param: param as [AnyHashable : Any])
......@@ -78,7 +78,7 @@ class RoomMessageManager: NSObject {
self.modifyMessage(message: message, dic: dic)
return
}
V2TIMManager.sharedInstance().findMessages([message.messageId]) { [weak self] messageArray in
V2TIMManager.sharedInstance().findMessages(messageIDList: [message.messageId]) { [weak self] messageArray in
guard let self = self else { return }
guard let array = messageArray else { return }
for previousMessage in array where previousMessage.msgID == message.messageId {
......@@ -92,14 +92,14 @@ class RoomMessageManager: NSObject {
private func modifyMessage(message: RoomMessageModel, dic:[String: Any]) {
guard let message = message.getMessage() else { return }
guard var customElemDic = TUITool.jsonData2Dictionary(message.customElem.data) as? [String: Any] else { return }
guard var customElemDic = TUITool.jsonData2Dictionary(message.customElem?.data) as? [String: Any] else { return }
for (key, value) in dic {
customElemDic[key] = value
}
guard let jsonString = customElemDic.convertToString() else { return }
let jsonData = jsonString.data(using: String.Encoding.utf8)
message.customElem.data = jsonData
V2TIMManager.sharedInstance().modifyMessage(message) { code, desc, msg in
message.customElem?.data = jsonData
V2TIMManager.sharedInstance().modifyMessage(msg: message) { code, desc, msg in
if code == 0 {
debugPrint("modifyMessage,success")
} else {
......
......@@ -62,7 +62,7 @@ class RoomMessageModel {
private func setMessageCustomElemData(dict: [String: Any], message: V2TIMMessage) {
guard let jsonString = dict.convertToString() else { return }
let jsonData = jsonString.data(using: String.Encoding.utf8)
message.customElem.data = jsonData
message.customElem?.data = jsonData
}
func getDictFromMessageModel() -> [String: Any] {
......
......@@ -76,7 +76,7 @@ extension RoomMessageExtensionObserver: TUIExtensionProtocol {
private extension String {
static var meetingText: String {
localized("Quick meeting")
localized("Quick conference")
}
static var roomDeviceSetText: String {
localized("Meeting Settings")
......
......@@ -33,7 +33,7 @@ class TUIRoomImAccessService: NSObject, TUIServiceProtocol {
}
extension TUIRoomImAccessService: V2TIMSignalingListener {
func onReceiveNewInvitation(_ inviteID: String, inviter: String, groupID: String, inviteeList: [String], data: String?) {
func onReceiveNewInvitation(inviteID: String, inviter: String?, groupID: String?, inviteeList: [String], data: String?) {
guard let data = data else { return }
let dict = data.convertToDic()
guard let businessID = dict?["businessID"] as? String else { return }
......
......@@ -173,7 +173,7 @@ class RoomMessageView: UIView {
}
func setupViewState() {
roomNameLabel.text = (viewModel.message.ownerName ) + .quickMeetingText
roomNameLabel.text = localizedReplace(.quickMeetingText, replace: viewModel.message.ownerName)
if viewModel.message.owner != viewModel.userId {
inviteUserButton.isHidden = true
} else {
......@@ -322,7 +322,5 @@ private extension String {
static var meetingEnded: String {
localized("Meeting ended")
}
static var quickMeetingText: String {
localized("Quick meeting")
}
static let quickMeetingText = localized("xx's quick conference")
}
......@@ -39,9 +39,9 @@ class ChatExtensionRoomSettingsViewModel {
}
private extension String {
static var cameraSetText: String {
localized("Join the meeting and start the camera")
localized("Enable video when joining a meeting")
}
static var micSeatText: String {
localized("Join the conference and turn on the mic")
localized("Enable audio when joining a meeting")
}
}
......@@ -60,7 +60,12 @@ class InvitedToJoinRoomViewModel: NSObject, AVAudioPlayerDelegate {
}
private func enterRoom() {
roomManager.enterRoom(roomId: roomId)
roomManager.enterRoom(roomId: roomId) {_ in
let vc = ConferenceMainViewController()
RoomRouter.shared.push(viewController: vc)
} onError: { code, message in
RoomRouter.makeToast(toast: code.description ?? message)
}
closeInvitedToJoinRoomView()
}
......
......@@ -31,9 +31,9 @@ class RoomMessageBubbleCellData: TUIBubbleMessageCellData {
override class func getDisplayString(_ message: V2TIMMessage) -> String {
let businessID = parseBusinessID(message: message)
if businessID == BussinessID_GroupRoomMessage {
let dict = TUITool.jsonData2Dictionary(message.customElem.data) as? [String: Any]
let dict = TUITool.jsonData2Dictionary(message.customElem?.data) as? [String: Any]
let userName = dict?["ownerName"] as? String ?? ""
return userName + .quickMeetingText
return localizedReplace(.quickMeetingText, replace: userName)
} else {
return super.getDisplayString(message)
}
......@@ -41,7 +41,7 @@ class RoomMessageBubbleCellData: TUIBubbleMessageCellData {
private class func parseBusinessID(message: V2TIMMessage?) -> String {
guard let message = message else { return "" }
let customData = message.customElem.data
let customData = message.customElem?.data
let dict = TUITool.jsonData2Dictionary(customData)
guard let businessID = dict?["businessID"] as? String else { return ""}
return businessID
......@@ -54,6 +54,6 @@ class RoomMessageBubbleCellData: TUIBubbleMessageCellData {
private extension String {
static var quickMeetingText: String {
localized("'s quick meeting")
localized("xx's quick conference")
}
}
......@@ -66,7 +66,15 @@ class RoomMessageViewModel: NSObject {
private func enterRoom() {
if !engineManager.store.isEnteredRoom {
roomManager.enterRoom(roomId: message.roomId)
roomManager.enterRoom(roomId: message.roomId) {_ in
let vc = ConferenceMainViewController()
RoomRouter.shared.push(viewController: vc)
} onError: { [weak self] code, errorMessage in
if let self = self, code == .roomIdNotExist {
self.messageManager.resendRoomMessage(message: self.message, dic: ["roomState": RoomMessageModel.RoomState.destroyed.rawValue])
}
RoomRouter.makeToast(toast: code.description ?? errorMessage)
}
} else {
EngineEventCenter.shared.notifyUIEvent(key: .TUIRoomKitService_ShowRoomMainView, param: [:])
}
......
......@@ -15,12 +15,17 @@ import Combine
#endif
import Factory
protocol FloatChatDisplayViewDelegate: AnyObject {
func getTheLatestUserName(userId: String) -> String
}
class FloatChatDisplayView: UIView {
@Injected(\.floatChatService) private var store: FloatChatStoreProvider
private lazy var messagePublisher = self.store.select(FloatChatSelectors.getLatestMessage)
private var messages: [FloatChatMessageView] = []
var cancellableSet = Set<AnyCancellable>()
private let messageSpacing: CGFloat = 8
weak var delegate: FloatChatDisplayViewDelegate?
private lazy var blurLayer: CALayer = {
let layer = CAGradientLayer()
......@@ -46,6 +51,7 @@ class FloatChatDisplayView: UIView {
guard !isViewReady else { return }
constructViewHierarchy()
bindInteraction()
reportViewShow()
isViewReady = true
}
......@@ -55,7 +61,7 @@ class FloatChatDisplayView: UIView {
func bindInteraction() {
messagePublisher
.filter{ !$0.content.isEmpty }
.filter{ !($0.content.isEmpty && $0.type == .text) }
.receive(on: DispatchQueue.mainQueue)
.sink { [weak self] floatMessage in
guard let self = self else { return }
......@@ -64,7 +70,15 @@ class FloatChatDisplayView: UIView {
.store(in: &cancellableSet)
}
private func reportViewShow() {
store.dispatch(action: FloatChatActions.reportData(payload: .metricsBarragePanelShow))
}
private func addMessage(_ message: FloatChatMessage) {
var message = message
if let userName = delegate?.getTheLatestUserName(userId: message.user.userId), !userName.isEmpty {
message.user.userName = userName
}
let messageView = FloatChatMessageView(floatMessage: message)
if currentMessageHeight() + messageView.height + messageSpacing > bounds.height {
removeOldestMessage()
......
......@@ -170,6 +170,7 @@ class FloatChatInputController: UIViewController {
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .inputCannotBeEmpty, position: .center)))
} else {
store.dispatch(action: FloatChatActions.sendMessage(payload: inputTextView.normalText))
store.dispatch(action: FloatChatActions.reportData(payload: .metricsBarrageSendMessage))
}
hideInputView()
}
......
......@@ -27,13 +27,13 @@ class FloatChatService: NSObject {
override init() {
super.init()
imManager?.addSimpleMsgListener(listener: self)
imManager?.addAdvancedMsgListener(listener: self)
}
func sendGroupMessage(_ message: String) -> AnyPublisher<String, Never> {
return Future<String, Never> { [weak self] promise in
guard let self = self else { return }
self.imManager?.sendGroupTextMessage(message, to: self.roomId, priority: .PRIORITY_NORMAL, succ: {
self.imManager?.sendGroupTextMessage(text: message, to: self.roomId, priority: .PRIORITY_NORMAL, succ: {
promise(.success((message)))
}, fail: { code, message in
let errorMsg = TUITool.convertIMError(Int(code), msg: message)
......@@ -45,13 +45,10 @@ class FloatChatService: NSObject {
}
}
extension FloatChatService: V2TIMSimpleMsgListener {
func onRecvGroupTextMessage(_ msgID: String!, groupID: String!, sender info: V2TIMGroupMemberInfo!, text: String!) {
guard groupID == roomId else {
return
}
let user = FloatChatUser(memberInfo: info)
let floatMessage = FloatChatMessage(user: user, content: text)
extension FloatChatService: V2TIMAdvancedMsgListener {
func onRecvNewMessage(msg: V2TIMMessage!) {
guard msg.groupID == roomId, let msg = msg else { return }
let floatMessage = FloatChatMessage(msg: msg)
store?.dispatch(action: FloatChatActions.onMessageReceived(payload: floatMessage))
}
}
......@@ -15,10 +15,19 @@ struct FloatChatState: Codable {
var latestMessage = FloatChatMessage()
}
enum FloatChatMessageType: Codable, Equatable {
case text
case image
case video
case file
}
struct FloatChatMessage: Codable, Equatable {
var id = UUID()
var user = FloatChatUser()
var type: FloatChatMessageType = .text
var content: String = ""
var fileName: String = ""
var extInfo: [String: AnyCodable] = [:]
init() {}
......@@ -27,6 +36,23 @@ struct FloatChatMessage: Codable, Equatable {
self.user = user
self.content = content
}
init(msg: V2TIMMessage) {
self.user = FloatChatUser(userId: msg.sender ?? "", userName: msg.nickName ?? "", avatarUrl: msg.faceURL ?? "")
switch msg.elemType {
case .ELEM_TYPE_TEXT:
self.type = .text
self.content = msg.textElem?.text ?? ""
case .ELEM_TYPE_IMAGE:
self.type = .image
case .ELEM_TYPE_VIDEO:
self.type = .video
case .ELEM_TYPE_FILE:
self.type = .file
self.fileName = msg.fileElem?.filename ?? ""
default: break
}
}
}
struct FloatChatUser: Codable, Equatable {
......
......@@ -14,6 +14,7 @@ enum FloatChatActions {
static let onMessageSended = ActionTemplate(id: key.appending(".messageSended"), payloadType: String.self)
static let onMessageReceived = ActionTemplate(id: key.appending(".messageReceived"), payloadType: FloatChatMessage.self)
static let setRoomId = ActionTemplate(id: key.appending(".setRoomId"), payloadType: String.self)
static let reportData = ActionTemplate(id: key.appending(".reportData"), payloadType: DataReport.self)
}
enum FloatViewActions {
......
......@@ -18,4 +18,12 @@ class FloatChatEffect: Effects {
}
.eraseToAnyPublisher()
}
let reportData = Effect<Environment>.nonDispatching { actions, environment in
actions
.wasCreated(from: FloatChatActions.reportData)
.sink { action in
RoomKitReport.reportData(action.payload)
}
}
}
......@@ -83,12 +83,26 @@ class FloatChatMessageView: UIView {
.foregroundColor: UIColor.tui_color(withHex: "B2BBD1")]
let userNameAttributedText = NSMutableAttributedString(string: userName,
attributes: userNameAttributes)
let contentAttributedText: NSMutableAttributedString = getFullContentAttributedText(content: message.content)
let content = getDisplayedContent(message: message)
let contentAttributedText: NSMutableAttributedString = getFullContentAttributedText(content: content)
userNameAttributedText.append(contentAttributedText)
return userNameAttributedText
}
private func getDisplayedContent(message: FloatChatMessage) -> String {
switch message.type {
case .text:
return message.content
case .image:
return .sentPicture
case .video:
return .sentVideo
case .file:
return localizedReplace(.sent, replace: message.fileName)
}
}
private func getFullContentAttributedText(content: String) -> NSMutableAttributedString {
return EmotionHelper.shared.obtainImagesAttributedString(byText: content,
font: UIFont(name: "PingFangSC-Regular", size: 12) ??
......@@ -96,3 +110,9 @@ class FloatChatMessageView: UIView {
}
}
private extension String {
static let sentPicture = localized("Sent a picture")
static let sentVideo = localized("Sent a video")
static let sent = localized("Sent xx")
}
//
// VideoSeatCell.swift
// Pods
//
// Created by CY zhao on 2024/11/11.
//
import SnapKit
import UIKit
import Combine
class MultiStreamCell: UICollectionViewCell {
var cancellableSet = Set<AnyCancellable>()
var videoItem: UserInfo?
var isSupportedAmplification: Bool {
return videoItem?.videoStreamType == .screenStream
}
private var isBorderHighlighted = false
private var lastVolumeUpdateTime: TimeInterval = 0
private lazy var scrollRenderView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = UIColor(0x17181F)
scrollView.layer.cornerRadius = 16
scrollView.layer.masksToBounds = true
scrollView.layer.borderWidth = 2
scrollView.layer.borderColor = UIColor.clear.cgColor
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.maximumZoomScale = 5
scrollView.minimumZoomScale = 1
scrollView.isScrollEnabled = false
scrollView.delegate = self
return scrollView
}()
let renderView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
return view
}()
let backgroundMaskView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = UIColor(0x17181F)
view.layer.cornerRadius = 16
view.layer.masksToBounds = true
return view
}()
let userInfoView: VideoUserStatusView = {
let view = VideoUserStatusView()
return view
}()
let avatarImageView: UIImageView = {
let imageView = UIImageView(frame: .zero)
imageView.layer.masksToBounds = true
return imageView
}()
private var isViewReady = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
contentView.backgroundColor = .clear
}
private func constructViewHierarchy() {
scrollRenderView.addSubview(renderView)
scrollRenderView.addSubview(backgroundMaskView)
contentView.addSubview(scrollRenderView)
contentView.addSubview(avatarImageView)
contentView.addSubview(userInfoView)
}
private func activateConstraints() {
scrollRenderView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(2)
}
renderView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.equalToSuperview()
make.height.equalToSuperview()
}
backgroundMaskView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
userInfoView.snp.makeConstraints { make in
make.height.equalTo(24)
make.bottom.equalToSuperview().offset(-5)
make.leading.equalToSuperview().offset(5)
make.width.lessThanOrEqualTo(self).multipliedBy(0.9)
}
}
func reset() {
videoItem = nil
cancellableSet.removeAll()
resetBorderColor()
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
scrollRenderView.zoomScale = 1.0
}
deinit {
NSObject.cancelPreviousPerformRequests(withTarget: self)
debugPrint("deinit \(self)")
}
}
extension MultiStreamCell: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return isSupportedAmplification ? renderView : nil
}
}
// MARK: - Public
extension MultiStreamCell {
func updateUI(item: UserInfo) {
videoItem = item
let placeholder = UIImage(named: "room_default_user", in: tuiRoomKitBundle(), compatibleWith: nil)
avatarImageView.sd_setImage(with: URL(string: item.avatarUrl), placeholderImage: placeholder)
avatarImageView.isHidden = item.videoStreamType == .screenStream ? true : item.hasVideoStream
backgroundMaskView.isHidden = item.videoStreamType == .screenStream ? true : item.hasVideoStream
userInfoView.updateUserStatus(item)
scrollRenderView.layer.borderColor = UIColor.clear.cgColor
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
let width = min(self.mm_w / 2, 72)
self.avatarImageView.layer.cornerRadius = width * 0.5
guard let _ = self.avatarImageView.superview else { return }
self.avatarImageView.snp.remakeConstraints { make in
make.height.width.equalTo(width)
make.center.equalToSuperview()
}
}
}
func updateUIVolume(item: UserInfo) {
guard videoItem?.userId == item.userId else { return }
videoItem?.hasAudioStream = item.hasAudioStream
userInfoView.updateUserVolume(hasAudio: item.hasAudioStream, volume: item.userVoiceVolume)
lastVolumeUpdateTime = Date().timeIntervalSince1970
if item.userVoiceVolume > 0 && item.hasAudioStream {
if item.videoStreamType != .screenStream {
if !isBorderHighlighted {
scrollRenderView.layer.borderColor = UIColor(0xA5FE33).cgColor
isBorderHighlighted = true
}
scheduleBorderReset()
}
} else {
resetBorderColor()
}
}
private func scheduleBorderReset() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
guard let self = self else { return }
let now = Date().timeIntervalSince1970
if now - self.lastVolumeUpdateTime >= 2 {
self.resetBorderColor()
}
}
}
private func resetBorderColor() {
scrollRenderView.layer.borderColor = UIColor.clear.cgColor
isBorderHighlighted = false
}
}
class VideoUserStatusView: UIView {
private var isShownHomeOwnerImageView: Bool = false
private var isViewReady: Bool = false
override func didMoveToWindow() {
super.didMoveToWindow()
guard !isViewReady else {
return
}
isViewReady = true
constructViewHierarchy()
activateConstraints()
backgroundColor = UIColor(0x22262E, alpha: 0.8)
layer.cornerRadius = 12
layer.masksToBounds = true
}
private func constructViewHierarchy() {
addSubview(homeOwnerImageView)
addSubview(voiceVolumeImageView)
addSubview(userNameLabel)
}
private let homeOwnerImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil))
imageView.layer.cornerRadius = 12
imageView.layer.masksToBounds = true
return imageView
}()
private let userNameLabel: UILabel = {
let user = UILabel()
user.textColor = .white
user.backgroundColor = UIColor.clear
user.textAlignment = isRTL ? .right : .left
user.numberOfLines = 1
user.font = UIFont(name: "PingFangSC-Regular", size: 12)
return user
}()
private let voiceVolumeImageView: VolumeView = {
let imageView = VolumeView()
return imageView
}()
private func activateConstraints() {
updateOwnerImageConstraints()
voiceVolumeImageView.snp.remakeConstraints { make in
make.leading.equalTo(homeOwnerImageView.snp.trailing).offset(6.scale375())
make.width.height.equalTo(14)
make.centerY.equalToSuperview()
}
userNameLabel.snp.makeConstraints { make in
make.leading.equalTo(voiceVolumeImageView.snp.trailing).offset(5)
make.centerY.equalToSuperview()
make.trailing.equalToSuperview().offset(-8)
}
}
private func updateOwnerImageConstraints() {
guard let _ = homeOwnerImageView.superview else { return }
homeOwnerImageView.snp.remakeConstraints { make in
make.leading.equalToSuperview()
make.width.height.equalTo(isShownHomeOwnerImageView ? 24 : 0)
make.top.bottom.equalToSuperview()
}
}
}
// MARK: - Public
extension VideoUserStatusView {
func updateUserStatus(_ item: UserInfo) {
if !item.userName.isEmpty {
userNameLabel.text = item.userName
} else {
userNameLabel.text = item.userId
}
if item.userRole == .roomOwner {
homeOwnerImageView.image = UIImage(named: "room_homeowner", in: tuiRoomKitBundle(), compatibleWith: nil)
} else if item.userRole == .administrator {
homeOwnerImageView.image = UIImage(named: "room_administrator", in: tuiRoomKitBundle(), compatibleWith: nil)
}
isShownHomeOwnerImageView = item.userRole != .generalUser
homeOwnerImageView.isHidden = !isShownHomeOwnerImageView
updateOwnerImageConstraints()
updateUserVolume(hasAudio: item.hasAudioStream, volume: item.userVoiceVolume)
}
func updateUserVolume(hasAudio: Bool, volume: Int) {
voiceVolumeImageView.updateVolume(CGFloat(volume))
voiceVolumeImageView.updateAudio(hasAudio)
}
}
//
// MultiStreamsSorter.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/12/18.
//
import Foundation
class MultiStreamsSorter {
private let currentUserId: String
private var roomOwnerId: String = ""
init(currentUserId: String) {
self.currentUserId = currentUserId
}
func sortStreams(_ videoItems: [UserInfo]) -> [UserInfo] {
guard needSorting(videoItems) else { return videoItems }
var sortedItems = videoItems
if let ownerIndex = sortedItems.firstIndex(where: { $0.userRole == .roomOwner }) {
let owner = sortedItems.remove(at: ownerIndex)
roomOwnerId = owner.userId
sortedItems.insert(owner, at: 0)
}
if currentUserId != roomOwnerId,
let currentUserIndex = sortedItems.firstIndex(where: { $0.userId == currentUserId }) {
let currentUser = sortedItems.remove(at: currentUserIndex)
sortedItems.insert(currentUser, at: 1)
}
let sortedOthers = sortedItems
.dropFirst(currentUserId == roomOwnerId ? 1 : 2)
.sorted { first, second in
let firstStatus = StreamStatus(hasAudio: first.hasAudioStream,
hasVideo: first.hasVideoStream)
let secondStatus = StreamStatus(hasAudio: second.hasAudioStream,
hasVideo: second.hasVideoStream)
return firstStatus.priority < secondStatus.priority
}
return Array(sortedItems.prefix(currentUserId == roomOwnerId ? 1 : 2)) + sortedOthers
}
private func needSorting(_ videoItems: [UserInfo]) -> Bool {
guard videoItems.count > 1 else { return false }
if let firstUser = videoItems.first,
firstUser.userRole != .roomOwner,
videoItems.contains(where: { $0.userRole == .roomOwner}) {
return true
}
if currentUserId != roomOwnerId {
let currentUserExpectedPosition = 1
if videoItems.count > currentUserExpectedPosition,
videoItems[currentUserExpectedPosition].userId != currentUserId,
videoItems.contains(where: { $0.userId == currentUserId }) {
return true
}
}
let startIndex = currentUserId == roomOwnerId ? 1 : 2
guard videoItems.count > startIndex + 1 else { return false }
let otherStreams = Array(videoItems[startIndex...])
for i in 0..<(otherStreams.count - 1) {
let current = otherStreams[i]
let next = otherStreams[i + 1]
let currentStreamState = StreamStatus(hasAudio: current.hasAudioStream, hasVideo: current.hasVideoStream)
let nexdtStreamState = StreamStatus(hasAudio: next.hasAudioStream, hasVideo: next.hasVideoStream)
if currentStreamState.priority > nexdtStreamState.priority {
return true
}
}
return false
}
}
enum StreamStatus {
case videoAndAudio
case videoOnly
case audioOnly
case none
init(hasAudio: Bool, hasVideo: Bool) {
switch(hasAudio, hasVideo) {
case(true, true): self = .videoAndAudio
case(false, true): self = .videoOnly
case(true, false): self = .audioOnly
case(false, false): self = .none
}
}
var priority: Int {
switch self {
case .videoAndAudio: return 0
case .videoOnly: return 1
case .audioOnly: return 2
case .none: return 3
}
}
}
//
// VideoStoreRegister.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/11/7.
//
import Foundation
import Factory
extension Container {
var videoStore: Factory<VideoStore> {
self { VideoStoreProvider() }.shared
}
}
//
// CGFloat+Extension.swift
// Alamofire
//
// Created by aby on 2022/12/26.
// Copyright © 2022 Tencent. All rights reserved.
......
//
// BellFeature.swift
// Pods
//
// Created by janejntang on 2024/11/20.
//
import AVFAudio
class BellFeature: NSObject, AVAudioPlayerDelegate {
private var audioPlayer: AVAudioPlayer?
private let placeholderBellName = "phone_ringing"
private let placeholderBellType = "mp3"
private lazy var customBellPath: String? = {
return ConferenceSession.sharedInstance.implementation.bellPath
}()
private lazy var enableMuteMode: Bool = {
return ConferenceSession.sharedInstance.implementation.enableMuteMode
}()
private var needPlayRingtone = false
override init() {
super.init()
registerNotifications()
}
private func registerNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleAppWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleAppDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc func handleInterruption(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
guard let userInfo = notification.userInfo else { return }
guard let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt else { return }
guard let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
switch type {
case .began:
self.audioPlayer?.pause()
case .ended:
let optinsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0
let options = AVAudioSession.InterruptionOptions(rawValue: optinsValue)
if options.contains(.shouldResume), !self.enableMuteMode, let audioPlayer = self.audioPlayer {
audioPlayer.play()
}
default: break
}
}
}
@objc func handleAppWillResignActive() {
audioPlayer?.pause()
}
@objc func handleAppDidBecomeActive() {
if needPlayRingtone {
playBell()
needPlayRingtone = false
} else if let audioPlayer = audioPlayer, !audioPlayer.isPlaying, !self.enableMuteMode {
audioPlayer.play()
}
}
func playBell() {
guard !enableMuteMode else { return }
guard let bellURL = findBellPath() else { return }
if UIApplication.shared.applicationState != .background {
playAudio(bellURL)
} else {
needPlayRingtone = true
}
}
func stopBell() {
audioPlayer?.stop()
audioPlayer = nil
needPlayRingtone = false
}
private func findBellPath() -> URL? {
let customBellPath = customBellPath?.replacingOccurrences(of: "file://", with: "")
if let bellPath = customBellPath, checkResourceExisted(path: bellPath) {
return URL(fileURLWithPath: bellPath)
} else {
return tuiRoomKitBundle().url(forResource: placeholderBellName, withExtension: placeholderBellType)
}
}
private func checkResourceExisted(path: String) -> Bool {
return FileManager.default.fileExists(atPath: path)
}
private func playAudio(_ audioURL: URL) {
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
} catch let error {
debugPrint("AVAudioSession set outputAudioPort error:\(error.localizedDescription)")
}
do {
try audioPlayer = AVAudioPlayer(contentsOf: audioURL)
audioPlayer?.numberOfLoops = -1
audioPlayer?.delegate = self
audioPlayer?.prepareToPlay()
} catch let error {
debugPrint("audioPlayer error: \(error.localizedDescription)")
}
audioPlayer?.play()
}
}
//
// vibrationFeature.swift
// Pods
//
// Created by janejntang on 2024/11/20.
//
import AVFAudio
class VibrationFeature {
private var vibrationTimer: Timer?
private let vibrationInterval = 0.1
private let maxVibrationTime = 60.0
private lazy var enableVibrationMode: Bool = {
return ConferenceSession.sharedInstance.implementation.enableVibrationMode
}()
func playVibrate() {
guard enableVibrationMode else { return }
var vibrationCount = 0
let vibrationTotalCount = Int(maxVibrationTime / vibrationInterval)
vibrationTimer = Timer.scheduledTimer(withTimeInterval: vibrationInterval, repeats: true) { [weak self] timer in
guard let self = self else { return }
guard UIApplication.shared.applicationState == .active else { return }
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
vibrationCount = vibrationCount + 1
if vibrationCount >= vibrationTotalCount {
self.stopVibrate()
}
}
}
func stopVibrate() {
vibrationTimer?.invalidate()
vibrationTimer = nil
}
deinit {
debugPrint("deinit:\(self)")
}
}
//
// RoomKitLog.swift
// TUIRoomKit
//
// Created by CY Zhao on 2024/12/12.
//
import Foundation
#if canImport(TXLiteAVSDK_TRTC)
import TXLiteAVSDK_TRTC
#elseif canImport(TXLiteAVSDK_Professional)
import TXLiteAVSDK_Professional
#endif
class RoomKitLog {
private static let API = "TuikitLog"
private static let LOG_KEY_API = "api"
private static let LOG_KEY_PARAMS = "params"
private static let LOG_KEY_PARAMS_LEVEL = "level"
private static let LOG_KEY_PARAMS_MESSAGE = "message"
private static let LOG_KEY_PARAMS_FILE = "file"
private static let LOG_KEY_PARAMS_LINE = "line"
private static let LOG_KEY_PARAMS_MODULE = "module"
private static let LOG_KEY_PARAMS_MODULE_VALUE = "TUIRoomKit"
private func `init`() {}
enum RoomKitLogLevel: Int {
case error = 2
case warn = 1
case info = 0
}
static func error(_ file: String, _ line: String, _ messages: String...) {
log(level: .error, file: file, line: line, messages)
}
static func warn(_ file: String, _ line: String, _ messages: String...) {
log(level: .warn, file: file, line: line, messages)
}
static func info(_ file: String, _ line: String, _ messages: String...) {
log(level: .info, file: file, line: line, messages)
}
private static func log(level: RoomKitLogLevel = .info, file: String, line: String, _ messages: [String]) {
let apiParams: [String: Any] = [
LOG_KEY_API: API,
LOG_KEY_PARAMS: [
LOG_KEY_PARAMS_LEVEL: level.rawValue,
LOG_KEY_PARAMS_MESSAGE: messages.joined(),
LOG_KEY_PARAMS_MODULE: LOG_KEY_PARAMS_MODULE_VALUE,
LOG_KEY_PARAMS_FILE: file,
LOG_KEY_PARAMS_LINE: Int(line) ?? 0,
],
]
let jsonData = try? JSONSerialization.data(withJSONObject: apiParams, options: .prettyPrinted)
guard let jsonData = jsonData, let jsonString = String(data: jsonData, encoding: .utf8) else { return }
TRTCCloud.sharedInstance().callExperimentalAPI(jsonString)
}
}
//
// RoomKitReport.swift
// Pods
//
// Created by janejntang on 2025/1/17.
//
import RTCRoomEngine
enum DataReport: Int {
case metricsBarragePanelShow = 106061
case metricsBarrageSendMessage = 106062
case metricsFloatWindowShow = 106063
case metricsUserListPanelShow = 106064
case metricsUserListSearch = 106065
case metricsSettingsPanelShow = 106066
case metricsChatPanelShow = 106057
case metricsShareRoomInfoPanelShow = 106067
case metricsConferenceSchedulePanelShow = 106068
case metricsConferenceModifyPanelShow = 106069
case metricsConferenceInfoPanelShow = 106070
case metricsConferenceAttendee = 106071
case metricsWaterMarkEnable = 106054
case metricsWaterMarkCustomText = 106060
}
class RoomKitReport {
private static let REPORT_API_KEY = "api"
private static let REPORT_API_VALUE = "KeyMetricsStats"
private static let REPORT_PARAMS = "params"
private static let REPORT_PARAMS_KEY = "key"
private func `init`() {}
static func reportData(_ dataReport: DataReport) {
let params: [String: Any] = [
REPORT_API_KEY: REPORT_API_VALUE,
REPORT_PARAMS: [
REPORT_PARAMS_KEY: dataReport.rawValue
]
]
guard let jsonString = params.convertToString() else { return }
TUIRoomEngine.callExperimentalAPI(jsonStr: jsonString)
}
}
......@@ -11,7 +11,7 @@ import Factory
enum ConferenceRoute {
case none
case schedule(memberSelectFactory: MemberSelectionFactory?)
case schedule
case main(conferenceParams: ConferenceParamType)
case selectMember(memberSelectParams: MemberSelectParams?)
case selectedMember(showDeleteButton: Bool, selectedMembers: [UserInfo])
......@@ -20,7 +20,7 @@ enum ConferenceRoute {
case modifySchedule(conferenceInfo: ConferenceInfo)
case popup(view: UIView)
case alert(state: AlertState)
case invitation(roomInfo: TUIRoomInfo, invitation: TUIInvitation)
case invitation(roomInfo: RoomInfo, invitation: TUIInvitation)
func hideNavigationBar() -> Bool {
switch self {
......@@ -43,11 +43,7 @@ enum ConferenceRoute {
self = .none
}
case is ScheduleConferenceViewController:
guard let vc = viewController as? ScheduleConferenceViewController else {
self = .none
break
}
self = .schedule(memberSelectFactory: vc.memberSelectionFactory)
self = .schedule
case _ as ContactViewProtocol:
self = .selectMember(memberSelectParams: nil)
case is SelectedMembersViewController:
......@@ -65,7 +61,7 @@ enum ConferenceRoute {
self = .modifySchedule(conferenceInfo: vc?.conferenceInfo ?? ConferenceInfo())
case is ConferenceInvitationViewController:
let vc = viewController as? ConferenceInvitationViewController
self = .invitation(roomInfo: vc?.roomInfo ?? TUIRoomInfo(), invitation: vc?.invitation ?? TUIInvitation())
self = .invitation(roomInfo: vc?.roomInfo ?? RoomInfo(), invitation: vc?.invitation ?? TUIInvitation())
case is PopupViewController:
let vc = viewController as? PopupViewController
self = .popup(view: vc?.contentView ?? viewController.view)
......@@ -90,8 +86,8 @@ extension ConferenceRoute {
vc.setJoinConferenceParams(params: joinConferenceParams)
}
return vc
case .schedule(memberSelectFactory: let factory):
return ScheduleConferenceViewController(memberSelectFactory: factory)
case .schedule:
return ScheduleConferenceViewController()
case .selectMember(memberSelectParams: let memberSelectParams):
guard let params = memberSelectParams else {
return UIViewController()
......@@ -131,10 +127,11 @@ extension ConferenceRoute {
}
func getSelectMemberViewController(participants: ConferenceParticipants) -> (ContactViewProtocol & UIViewController)? {
guard let vc = Container.shared.contactViewController(participants) as? (ContactViewProtocol & UIViewController) else {
return nil
if ConferenceSession.sharedInstance.implementation.hasCustomContacts {
return Container.shared.contactViewController.resolve(participants) as? (ContactViewProtocol & UIViewController)
} else {
return SelectMemberViewController(participants: participants)
}
return vc
}
}
......
......@@ -18,13 +18,11 @@ typealias ConferenceNavigation = NavigationAction<ConferenceRoute>
struct ConferenceRouteState {
var currentRouteAction: ConferenceNavigation = .presented(route: .none)
var currentRoute: ConferenceRoute = .none
var memberSelectionFactory: MemberSelectionFactory?
}
enum ConferenceNavigationAction {
static let key = "conference.navigation.action"
static let navigate = ActionTemplate(id: key.appending("navigate"), payloadType: ConferenceNavigation.self)
static let setMemberSelectionFactory = ActionTemplate(id: key.appending(".setMemberSelectionFactory"), payloadType: MemberSelectionFactory.self)
}
let routeReducer = Reducer<ConferenceRouteState>(
......@@ -36,8 +34,5 @@ let routeReducer = Reducer<ConferenceRouteState>(
default:
break
}
}),
ReduceOn(ConferenceNavigationAction.setMemberSelectionFactory, reduce: { state, action in
state.memberSelectionFactory = action.payload
})
)
......@@ -20,7 +20,6 @@ protocol Route: ActionDispatcher {
}
private let currentRouteActionSelector = Selector(keyPath: \ConferenceRouteState.currentRouteAction)
private let memberSelectFactorySelector = Selector(keyPath: \ConferenceRouteState.memberSelectionFactory)
class ConferenceRouter: NSObject {
override init() {
......@@ -116,9 +115,9 @@ extension ConferenceRouter: Route {
}
func showContactView(delegate: ContactViewSelectDelegate, participants: ConferenceParticipants) {
guard let factory = store.selectCurrent(memberSelectFactorySelector) else { return }
let selectParams = MemberSelectParams(participants: participants, delegate: delegate, factory: factory)
let selectParams = MemberSelectParams(participants: participants, delegate: delegate)
store.dispatch(action: ConferenceNavigationAction.navigate(payload: .push(route: .selectMember(memberSelectParams: selectParams))))
RoomKitReport.reportData(.metricsConferenceAttendee)
}
func dispatch(action: Action) {
......
......@@ -64,11 +64,12 @@ class ConferenceOptions {
private static func createRoomInfo(startConferenceParams: StartConferenceParams) -> TUIRoomInfo {
let roomInfo = TUIRoomInfo()
roomInfo.roomId = startConferenceParams.roomId
roomInfo.isMicrophoneDisableForAllUser = !startConferenceParams.isOpenMicrophone
roomInfo.isCameraDisableForAllUser = !startConferenceParams.isOpenCamera
roomInfo.isMicrophoneDisableForAllUser = startConferenceParams.isMicrophoneDisableForAllUser
roomInfo.isCameraDisableForAllUser = startConferenceParams.isCameraDisableForAllUser
roomInfo.isSeatEnabled = startConferenceParams.isSeatEnabled
roomInfo.name = startConferenceParams.name ?? ""
roomInfo.seatMode = .applyToTake
roomInfo.password = startConferenceParams.password ?? ""
return roomInfo
}
......
......@@ -20,6 +20,7 @@ import RTCRoomEngine
public var isSeatEnabled = false
public var name: String?
public var password: String?
public init(roomId: String,
isOpenMicrophone: Bool = true,
......@@ -28,7 +29,8 @@ import RTCRoomEngine
isMicrophoneDisableForAllUser: Bool = false,
isCameraDisableForAllUser: Bool = false,
isSeatEnabled: Bool = false,
name: String? = nil) {
name: String? = nil,
password: String? = nil) {
self.roomId = roomId
self.isOpenMicrophone = isOpenMicrophone
self.isOpenCamera = isOpenCamera
......@@ -37,6 +39,7 @@ import RTCRoomEngine
self.isCameraDisableForAllUser = isCameraDisableForAllUser
self.isSeatEnabled = isSeatEnabled
self.name = name
self.password = password
super.init()
}
}
......@@ -65,6 +68,22 @@ import RTCRoomEngine
@objc public protocol ConferenceObserver {
@objc optional func onConferenceStarted(roomInfo: TUIRoomInfo, error: TUIError, message: String)
@objc optional func onConferenceJoined(roomInfo: TUIRoomInfo, error: TUIError, message: String)
@objc optional func onConferenceFinished(roomId: String)
@objc optional func onConferenceExited(roomId: String)
@objc optional func onConferenceFinished(roomInfo: TUIRoomInfo, reason: ConferenceFinishedReason)
@objc optional func onConferenceExited(roomInfo: TUIRoomInfo, reason: ConferenceExitedReason)
}
@objc
public enum ConferenceFinishedReason: Int {
case finishedByOwner
case finishedByServer
}
@objc
public enum ConferenceExitedReason: Int {
case exitedBySelf
case exitedByAdminKickOut
case exitedByServerKickOut
case exitedByJoinedOnOtherDevice
case exitedByKickedOutOfLine
case exitedByUserSigExpired
}
......@@ -18,13 +18,10 @@ struct ConferenceSection {
@objcMembers public class ConferenceListView: UIView {
// MARK: - Intailizer
public init(viewController: UIViewController, memberSelectFactory: MemberSelectionFactory?) {
public init(viewController: UIViewController) {
super.init(frame: .zero)
let viewRoute = ConferenceRoute.init(viewController: viewController)
navigation.initializeRoute(viewController: viewController, rootRoute: viewRoute)
if let factory = memberSelectFactory {
navigation.dispatch(action: ConferenceNavigationAction.setMemberSelectionFactory(payload: factory))
}
}
@available(*, unavailable, message: "Use init(viewController:) instead")
......@@ -160,6 +157,7 @@ struct ConferenceSection {
private func bindInteraction() {
subscribeToast()
subscribeScheduleSubject()
subscribeRoomSubject()
store.dispatch(action: ConferenceListActions.fetchConferenceList(payload: (fetchListCursor, conferencesPerFetch)))
conferenceListPublisher
.receive(on: DispatchQueue.global(qos: .default))
......@@ -345,6 +343,18 @@ extension ConferenceListView {
}
.store(in: &cancellableSet)
}
private func subscribeRoomSubject() {
store.roomActionSubject
.receive(on: RunLoop.main)
.filter { $0.id == RoomResponseActions.onExitSuccess.id }
.sink { [weak self] action in
guard let self = self else { return }
self.store.dispatch(action: ScheduleViewActions.refreshConferenceList())
}
.store(in: &cancellableSet)
}
}
private extension String {
......
......@@ -29,12 +29,7 @@ import TUICore
super.viewDidLoad()
RoomRouter.shared.initializeNavigationController(rootViewController: self)
RoomVideoFloatView.dismiss()
#if RTCube_APPSTORE
let selector = NSSelectorFromString("showAlertUserLiveTips")
if responds(to: selector) {
perform(selector)
}
#endif
viewModel.onViewDidLoadAction()
subscribeToast()
}
......
......@@ -34,5 +34,24 @@ import Foundation
@objc public func setContactsViewProvider(_ provider: @escaping (ConferenceParticipants) -> ContactViewProtocol) {
implementation.setContactsViewProvider(provider)
}
@objc public func setCallingBell(filePath: String){
implementation.setCallingBell(filePath: filePath)
}
@objc public func enableMuteMode(enable: Bool) {
implementation.enableMuteMode(enable: enable)
}
@objc public func enableVibrationMode(enable: Bool) {
implementation.enableVibrationMode(enable: enable)
}
@objc public func setAppGroup(_ appGroup: String) {
implementation.setAppGroup(appGroup)
}
@objc public func setParticipants(_ participants: [User]) {
implementation.setParticipants(participants)
}
}
......@@ -11,11 +11,8 @@ import RTCRoomEngine
import Combine
import TUICore
public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol
@objcMembers public class ScheduleConferenceViewController: UIViewController {
private var cancellableSet = Set<AnyCancellable>()
let memberSelectionFactory: MemberSelectionFactory?
private let durationTime = 1800
private let reminderSecondsBeforeStart = 600
lazy var rootView: ScheduleConferenceTableView = {
......@@ -30,18 +27,6 @@ public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol
return .portrait
}
public init(memberSelectFactory: MemberSelectionFactory?) {
self.memberSelectionFactory = memberSelectFactory
super.init(nibName: nil, bundle: nil)
if let factory = memberSelectFactory {
route.dispatch(action: ConferenceNavigationAction.setMemberSelectionFactory(payload: factory))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func loadView() {
self.view = rootView
}
......@@ -51,6 +36,7 @@ public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol
initializeView()
initializeRoute()
initializeData()
reportSchedulePanelShow()
subscribeScheduleSubject()
subscribeToast()
......@@ -125,6 +111,10 @@ public typealias MemberSelectionFactory = ([User]) -> ContactViewProtocol
return String(randomNumber)
}
private func reportSchedulePanelShow() {
store.reportViewShow(dataReport: .metricsConferenceSchedulePanelShow)
}
deinit {
debugPrint("deinit \(self)")
}
......
//
// AudioService.swift
// Pods
//
// Created by janejntang on 2024/12/26.
//
import RTCRoomEngine
import Combine
class AudioService {
private let engine = TUIRoomEngine.sharedInstance()
private let engineManeger = EngineManager.shared //TODO: replace later
func muteLocalAudio() {
engine.muteLocalAudio()
}
func unmuteLocalAudio() -> AnyPublisher<Void, RoomError> {
return Future<Void, RoomError> { [weak self] promise in
guard let self = self else { return }
engine.unmuteLocalAudio {
promise(.success(()))
} onError: { err, message in
let error = RoomError(error: err, message: message, showToast: false)
promise(.failure(error))
}
}
.eraseToAnyPublisher()
}
func openLocalMicrophone() -> AnyPublisher<Void, RoomError> {
return Future<Void, RoomError> { [weak self] promise in
guard let self = self else { return }
engineManeger.openLocalMicrophone {
promise(.success(()))
} onError: { err, message in
let error = RoomError(error: err, message: message, showToast: false)
promise(.failure(error))
}
}
.eraseToAnyPublisher()
}
}
......@@ -84,6 +84,7 @@ extension ConferenceInvitationService: TUIConferenceInvitationObserver {
func onInvitationHandledByOtherDevice(roomInfo: TUIRoomInfo, accepted: Bool) {
guard let store = self.store else { return }
store.dispatch(action: InvitationViewActions.dismissInvitationView())
store.dispatch(action: InvitationObserverActions.stopCallingBellAndVibration())
}
func onInvitationCancelled(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
......@@ -101,6 +102,7 @@ extension ConferenceInvitationService: TUIConferenceInvitationObserver {
func onInvitationTimeout(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
guard let store = self.store else { return }
store.dispatch(action: InvitationViewActions.dismissInvitationView())
store.dispatch(action: InvitationObserverActions.stopCallingBellAndVibration())
}
func onInvitationRevokedByAdmin(roomInfo: TUIRoomInfo, invitation: TUIInvitation, admin: TUIUserInfo) {
......
......@@ -149,6 +149,7 @@ extension ConferenceListService: TUIConferenceListManagerObserver {
let currentList = store.selectCurrent(ConferenceListSelectors.getConferenceList).map { $0.basicInfo.roomId }
if currentList.contains(roomId) {
store.dispatch(action: ConferenceListActions.removeConference(payload: roomId))
store.dispatch(action: ScheduleResponseActions.onConferenceRemoved(payload: roomId))
}
}
......
......@@ -21,3 +21,56 @@ struct RoomError: Error {
}
}
}
protocol LocalizedError {
var description: String?{get}
var isCommon: Bool{get}
}
extension TUIError: LocalizedError {
var description: String? {
switch self {
case .roomIdNotExist:
return .roomIdNotExist
case .roomIdOccupied:
return .roomIdOccupied
case .roomUserFull:
return .roomUserFull
case .roomNameInvalid:
return .roomNameInvalid
case .roomIdInvalid:
return .roomIdInvalid
case .operationInvalidBeforeEnterRoom:
return .operationInvalidBeforeEnterRoom
case .operationNotSupportedInCurrentRoomType:
return .operationNotSupportedInCurrentRoomType
case .alreadyInOtherRoom:
return .alreadyInOtherRoom
default:
return nil
}
}
var isCommon: Bool {
switch self {
case .roomIdNotExist, .roomIdOccupied, .roomUserFull:
return true
default:
return false
}
}
}
private extension String {
static let roomIdNotExist = localized("The room does not exist, please confirm the room ID or create a room!")
static let operationInvalidBeforeEnterRoom = localized("You need to enter the room to use this function.")
static let operationNotSupportedInCurrentRoomType = localized("This operation is not supported in the current room type.")
static let roomIdInvalid = localized("The room number is invalid. It must be printable ASCII characters and cannot exceed 48 bytes.")
static let roomIdOccupied = localized("The room ID is occupied, please select another room ID.")
static let roomNameInvalid = localized("The room name is invalid. It cannot exceed 30 bytes. If it contains Chinese characters, the character encoding must be UTF-8.")
static let alreadyInOtherRoom = localized("You are already in another room and need to leave the room before joining a new room.")
static let roomUserFull = localized("The room is full and you cannot enter the room temporarily.")
}
......@@ -8,15 +8,40 @@
import Foundation
import RTCRoomEngine
import Factory
import TUICore
import Combine
class InvitationObserverService: NSObject {
static let shared = InvitationObserverService()
public class InvitationObserverService: NSObject {
@objc public static let shared = InvitationObserverService()
private var invitationWindow: UIWindow?
private var getInvitationListCursor: String = ""
private let singleFetchCount: Int = 20
private let bellFeature = BellFeature()
private let vibrationFeature = VibrationFeature()
private override init() {
}
func showInvitationWindow(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
@objc public func show(extString: String) {
let store = Container.shared.conferenceStore()
let isEnteredRoom = store.selectCurrent(RoomSelectors.getIsEnteredRoom)
if isEnteredRoom || invitationWindow != nil {
return
}
guard let dict = extString.convertToDic() else { return }
guard let roomId = dict["RoomId"] as? String else { return }
guard let roomName = dict["RoomName"] as? String else { return }
guard let ownerName = dict["OwnerName"] as? String else { return }
guard let memberCount = dict["MemberCount"] as? Int else { return }
var roomInfo = RoomInfo()
roomInfo.roomId = roomId
roomInfo.name = roomName
roomInfo.ownerName = ownerName
roomInfo.memberCount = memberCount
self.getInvitationList(roomInfo: roomInfo)
}
func showInvitationWindow(roomInfo: RoomInfo, invitation: TUIInvitation) {
DispatchQueue.main.async {
let invitationViewController = ConferenceInvitationViewController(roomInfo: roomInfo, invitation: invitation)
self.invitationWindow = UIWindow()
......@@ -33,39 +58,72 @@ class InvitationObserverService: NSObject {
self.invitationWindow = nil
}
}
func playCallingBellAndVibration() {
bellFeature.playBell()
vibrationFeature.playVibrate()
}
func stopCallingBellAndVibration() {
bellFeature.stopBell()
vibrationFeature.stopVibrate()
}
private func getInvitationList(roomInfo: RoomInfo) {
let invitationManager = TUIRoomEngine.sharedInstance().getExtension(extensionType: .conferenceInvitationManager) as? TUIConferenceInvitationManager
let selfUserId = TUILogin.getUserID()
invitationManager?.getInvitationList(roomInfo.roomId, cursor: getInvitationListCursor, count: singleFetchCount) { [weak self] invitations, cursor in
guard let self = self else { return }
for invitation in invitations {
if invitation.invitee.userId != selfUserId {
continue
}
if invitation.status == .pending {
self.showInvitationWindow(roomInfo: roomInfo, invitation: invitation)
self.playCallingBellAndVibration()
return
}
}
self.getInvitationListCursor = cursor
if !cursor.isEmpty {
self.getInvitationList(roomInfo: roomInfo)
}
} onError: { error, message in
}
}
}
extension InvitationObserverService: TUIConferenceInvitationObserver {
func onReceiveInvitation(roomInfo: TUIRoomInfo, invitation: TUIInvitation, extensionInfo: String) {
public func onReceiveInvitation(roomInfo: TUIRoomInfo, invitation: TUIInvitation, extensionInfo: String) {
let store = Container.shared.conferenceStore()
store.dispatch(action: ConferenceInvitationActions.onReceiveInvitation(payload: (roomInfo, invitation)))
}
func onInvitationHandledByOtherDevice(roomInfo: TUIRoomInfo, accepted: Bool) {
public func onInvitationHandledByOtherDevice(roomInfo: TUIRoomInfo, accepted: Bool) {
}
func onInvitationCancelled(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
public func onInvitationCancelled(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
}
func onInvitationAccepted(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
public func onInvitationAccepted(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
}
func onInvitationRejected(roomInfo: TUIRoomInfo, invitation: TUIInvitation, reason: TUIInvitationRejectedReason) {
public func onInvitationRejected(roomInfo: TUIRoomInfo, invitation: TUIInvitation, reason: TUIInvitationRejectedReason) {
}
func onInvitationTimeout(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
public func onInvitationTimeout(roomInfo: TUIRoomInfo, invitation: TUIInvitation) {
}
func onInvitationRevokedByAdmin(roomInfo: TUIRoomInfo, invitation: TUIInvitation, admin: TUIUserInfo) {
public func onInvitationRevokedByAdmin(roomInfo: TUIRoomInfo, invitation: TUIInvitation, admin: TUIUserInfo) {
}
func onInvitationAdded(roomId: String, invitation: TUIInvitation) {
public func onInvitationAdded(roomId: String, invitation: TUIInvitation) {
}
func onInvitationRemoved(roomId: String, invitation: TUIInvitation) {
public func onInvitationRemoved(roomId: String, invitation: TUIInvitation) {
}
func onInvitationStatusChanged(roomId: String, invitation: TUIInvitation) {
public func onInvitationStatusChanged(roomId: String, invitation: TUIInvitation) {
}
}
......
......@@ -18,5 +18,6 @@ class ServiceCenter: NSObject {
let conferenceListService = ConferenceListService()
let roomService = RoomService()
let conferenceInvitationService = ConferenceInvitationService()
let audioService = AudioService()
}
......@@ -16,11 +16,13 @@ struct RoomInfo {
var ownerAvatarUrl = ""
var isSeatEnabled = false
var password = ""
var isMicrophoneDisableForAllUser = true
var isCameraDisableForAllUser = true
var isMicrophoneDisableForAllUser = false
var isCameraDisableForAllUser = false
var isScreenShareDisableForAllUser = false
var createTime: UInt = 0
var isPasswordEnabled: Bool = false
var isEnteredRoom = false
var memberCount = 0
init() {}
init(with roomInfo: TUIRoomInfo) {
......@@ -35,6 +37,8 @@ struct RoomInfo {
self.isCameraDisableForAllUser = roomInfo.isCameraDisableForAllUser
self.createTime = roomInfo.createTime
self.isPasswordEnabled = roomInfo.password.count > 0
self.memberCount = roomInfo.memberCount
self.isScreenShareDisableForAllUser = roomInfo.isScreenShareDisableForAllUser
}
}
......@@ -48,5 +52,6 @@ extension TUIRoomInfo {
self.isMicrophoneDisableForAllUser = roomInfo.isMicrophoneDisableForAllUser
self.isCameraDisableForAllUser = roomInfo.isCameraDisableForAllUser
self.seatMode = .applyToTake
self.isScreenShareDisableForAllUser = roomInfo.isScreenShareDisableForAllUser
}
}
......@@ -22,8 +22,8 @@ enum ConferenceInvitationActions {
// MARK: callback
static let updateInvitationList = ActionTemplate(id: key.appending(".setInvitationList"), payloadType: [TUIInvitation].self)
static let addInvitation = ActionTemplate(id: key.appending(".addInvitation"), payloadType: TUIInvitation.self)
static let removeInvitation = ActionTemplate(id: key.appending(".addInvitation"), payloadType: String.self)
static let changeInvitationStatus = ActionTemplate(id: key.appending(".addInvitation"), payloadType: TUIInvitation.self)
static let removeInvitation = ActionTemplate(id: key.appending(".removeInvitation"), payloadType: String.self)
static let changeInvitationStatus = ActionTemplate(id: key.appending(".changeInvitationStatus"), payloadType: TUIInvitation.self)
static let onInviteSuccess = ActionTemplate(id: key.appending("onInviteSuccess"))
static let onAcceptSuccess = ActionTemplate(id: key.appending("onAcceptSuccess"), payloadType: String.self)
static let onRejectSuccess = ActionTemplate(id: key.appending("onRejectSuccess"))
......
......@@ -36,6 +36,7 @@ class ConferenceInvitationEffects: Effects {
}
.catch { error -> Just<Action> in
environment.store?.dispatch(action: InvitationViewActions.dismissInvitationView())
environment.store?.dispatch(action: InvitationObserverActions.stopCallingBellAndVibration())
return Just(ErrorActions.throwError(payload: error))
}
}
......@@ -112,6 +113,7 @@ class ConferenceInvitationEffects: Effects {
RoomRouter.shared.push(viewController: vc)
}
environment.store?.dispatch(action: InvitationViewActions.dismissInvitationView())
environment.store?.dispatch(action: InvitationObserverActions.stopCallingBellAndVibration())
}
}
......@@ -128,7 +130,8 @@ class ConferenceInvitationEffects: Effects {
} else if isNotBeingInviting == false {
environment.store?.dispatch(action: ConferenceInvitationActions.reject(payload: (roomInfo.roomId, .rejectToEnter)))
} else {
InvitationObserverService.shared.showInvitationWindow(roomInfo: roomInfo, invitation: invitation)
InvitationObserverService.shared.showInvitationWindow(roomInfo: RoomInfo(with: roomInfo), invitation: invitation)
environment.store?.dispatch(action: InvitationObserverActions.playCallingBellAndVibration())
}
}
}
......
......@@ -12,4 +12,5 @@ enum RoomSelectors {
static let getRoomId = Selector.with(getRoomState, keyPath:\RoomInfo.roomId)
static let getIsEnteredRoom = Selector.with(getRoomState, keyPath:\RoomInfo.isEnteredRoom)
static let getIsScreenShareDisableForAllUser = Selector.with(getRoomState, keyPath: \RoomInfo.isScreenShareDisableForAllUser)
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment