mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-04-17 20:08:31 +00:00
Add mosaic mode to conference call view
This commit is contained in:
parent
b16372c420
commit
1f0c3fa5f7
13 changed files with 1165 additions and 370 deletions
21
Linphone/Assets.xcassets/picture-in-picture.imageset/Contents.json
vendored
Normal file
21
Linphone/Assets.xcassets/picture-in-picture.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "picture-in-picture.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Linphone/Assets.xcassets/picture-in-picture.imageset/picture-in-picture.svg
vendored
Normal file
1
Linphone/Assets.xcassets/picture-in-picture.imageset/picture-in-picture.svg
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M216,48H40A16,16,0,0,0,24,64V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,64H216v56H136a8,8,0,0,0-8,8v64H40ZM216,192H144V136h72v56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 280 B |
21
Linphone/Assets.xcassets/plus.imageset/Contents.json
vendored
Normal file
21
Linphone/Assets.xcassets/plus.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "plus.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Linphone/Assets.xcassets/plus.imageset/plus.svg
vendored
Normal file
1
Linphone/Assets.xcassets/plus.imageset/plus.svg
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 236 B |
21
Linphone/Assets.xcassets/squares-four.imageset/Contents.json
vendored
Normal file
21
Linphone/Assets.xcassets/squares-four.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "squares-four.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Linphone/Assets.xcassets/squares-four.imageset/squares-four.svg
vendored
Normal file
1
Linphone/Assets.xcassets/squares-four.imageset/squares-four.svg
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M104,40H56A16,16,0,0,0,40,56v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,104,40Zm0,64H56V56h48v48Zm96-64H152a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,64H152V56h48v48Zm-96,32H56a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V152A16,16,0,0,0,104,136Zm0,64H56V152h48v48Zm96-64H152a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V152A16,16,0,0,0,200,136Zm0,64H152V152h48v48Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 576 B |
21
Linphone/Assets.xcassets/waveform.imageset/Contents.json
vendored
Normal file
21
Linphone/Assets.xcassets/waveform.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "waveform.svg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Linphone/Assets.xcassets/waveform.imageset/waveform.svg
vendored
Normal file
1
Linphone/Assets.xcassets/waveform.imageset/waveform.svg
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M56,96v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0ZM88,24a8,8,0,0,0-8,8V224a8,8,0,0,0,16,0V32A8,8,0,0,0,88,24Zm40,32a8,8,0,0,0-8,8V192a8,8,0,0,0,16,0V64A8,8,0,0,0,128,56Zm40,32a8,8,0,0,0-8,8v64a8,8,0,0,0,16,0V96A8,8,0,0,0,168,88Zm40-16a8,8,0,0,0-8,8v96a8,8,0,0,0,16,0V80A8,8,0,0,0,208,72Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 404 B |
|
|
@ -218,6 +218,9 @@
|
||||||
},
|
},
|
||||||
"Attended transfer" : {
|
"Attended transfer" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Audio seulement" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Block the address" : {
|
"Block the address" : {
|
||||||
|
|
||||||
|
|
@ -501,6 +504,9 @@
|
||||||
},
|
},
|
||||||
"Missed call" : {
|
"Missed call" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Mosaïque" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"New call" : {
|
"New call" : {
|
||||||
|
|
||||||
|
|
@ -555,6 +561,9 @@
|
||||||
},
|
},
|
||||||
"Partager le lien" : {
|
"Partager le lien" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Participant actif" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Participants" : {
|
"Participants" : {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import UniformTypeIdentifiers
|
||||||
|
|
||||||
// swiftlint:disable type_body_length
|
// swiftlint:disable type_body_length
|
||||||
// swiftlint:disable line_length
|
// swiftlint:disable line_length
|
||||||
|
// swiftlint:disable file_length
|
||||||
struct CallView: View {
|
struct CallView: View {
|
||||||
|
|
||||||
@ObservedObject private var coreContext = CoreContext.shared
|
@ObservedObject private var coreContext = CoreContext.shared
|
||||||
|
|
@ -39,7 +40,9 @@ struct CallView: View {
|
||||||
let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
|
let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
|
||||||
|
|
||||||
@State var audioRouteSheet: Bool = false
|
@State var audioRouteSheet: Bool = false
|
||||||
@State var options: Int = 1
|
@State var changeLayoutSheet: Bool = false
|
||||||
|
@State var optionsAudioRoute: Int = 1
|
||||||
|
@State var optionsChangeLayout: Int = 2
|
||||||
@State var imageAudioRoute: String = ""
|
@State var imageAudioRoute: String = ""
|
||||||
@State var angleDegree = 0.0
|
@State var angleDegree = 0.0
|
||||||
@State var showingDialer = false
|
@State var showingDialer = false
|
||||||
|
|
@ -64,7 +67,13 @@ struct CallView: View {
|
||||||
.sheet(isPresented: $audioRouteSheet, onDismiss: {
|
.sheet(isPresented: $audioRouteSheet, onDismiss: {
|
||||||
audioRouteSheet = false
|
audioRouteSheet = false
|
||||||
}) {
|
}) {
|
||||||
innerBottomSheet()
|
audioRouteBottomSheet()
|
||||||
|
.presentationDetents([.fraction(0.3)])
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $changeLayoutSheet, onDismiss: {
|
||||||
|
changeLayoutSheet = false
|
||||||
|
}) {
|
||||||
|
changeLayoutBottomSheet()
|
||||||
.presentationDetents([.fraction(0.3)])
|
.presentationDetents([.fraction(0.3)])
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingDialer) {
|
.sheet(isPresented: $showingDialer) {
|
||||||
|
|
@ -81,7 +90,13 @@ struct CallView: View {
|
||||||
.sheet(isPresented: $audioRouteSheet, onDismiss: {
|
.sheet(isPresented: $audioRouteSheet, onDismiss: {
|
||||||
audioRouteSheet = false
|
audioRouteSheet = false
|
||||||
}) {
|
}) {
|
||||||
innerBottomSheet()
|
audioRouteBottomSheet()
|
||||||
|
.presentationDetents([.fraction(0.3)])
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $changeLayoutSheet, onDismiss: {
|
||||||
|
changeLayoutSheet = false
|
||||||
|
}) {
|
||||||
|
changeLayoutBottomSheet()
|
||||||
.presentationDetents([.fraction(0.3)])
|
.presentationDetents([.fraction(0.3)])
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingDialer) {
|
.sheet(isPresented: $showingDialer) {
|
||||||
|
|
@ -95,10 +110,15 @@ struct CallView: View {
|
||||||
} else {
|
} else {
|
||||||
innerView(geometry: geo)
|
innerView(geometry: geo)
|
||||||
.halfSheet(showSheet: $audioRouteSheet) {
|
.halfSheet(showSheet: $audioRouteSheet) {
|
||||||
innerBottomSheet()
|
audioRouteBottomSheet()
|
||||||
} onDismiss: {
|
} onDismiss: {
|
||||||
audioRouteSheet = false
|
audioRouteSheet = false
|
||||||
}
|
}
|
||||||
|
.halfSheet(showSheet: $changeLayoutSheet) {
|
||||||
|
changeLayoutBottomSheet()
|
||||||
|
} onDismiss: {
|
||||||
|
changeLayoutSheet = false
|
||||||
|
}
|
||||||
.halfSheet(showSheet: $showingDialer) {
|
.halfSheet(showSheet: $showingDialer) {
|
||||||
DialerBottomSheet(
|
DialerBottomSheet(
|
||||||
startCallViewModel: StartCallViewModel(),
|
startCallViewModel: StartCallViewModel(),
|
||||||
|
|
@ -149,10 +169,10 @@ struct CallView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func innerBottomSheet() -> some View {
|
func audioRouteBottomSheet() -> some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
options = 1
|
optionsAudioRoute = 1
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
||||||
|
|
@ -166,7 +186,7 @@ struct CallView: View {
|
||||||
}
|
}
|
||||||
}, label: {
|
}, label: {
|
||||||
HStack {
|
HStack {
|
||||||
Image(options == 1 ? "radio-button-fill" : "radio-button")
|
Image(optionsAudioRoute == 1 ? "radio-button-fill" : "radio-button")
|
||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
.resizable()
|
.resizable()
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
@ -189,7 +209,7 @@ struct CallView: View {
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
options = 2
|
optionsAudioRoute = 2
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
|
||||||
|
|
@ -198,7 +218,7 @@ struct CallView: View {
|
||||||
}
|
}
|
||||||
}, label: {
|
}, label: {
|
||||||
HStack {
|
HStack {
|
||||||
Image(options == 2 ? "radio-button-fill" : "radio-button")
|
Image(optionsAudioRoute == 2 ? "radio-button-fill" : "radio-button")
|
||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
.resizable()
|
.resizable()
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
@ -221,7 +241,7 @@ struct CallView: View {
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
options = 3
|
optionsAudioRoute = 3
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
||||||
|
|
@ -231,7 +251,7 @@ struct CallView: View {
|
||||||
}
|
}
|
||||||
}, label: {
|
}, label: {
|
||||||
HStack {
|
HStack {
|
||||||
Image(options == 3 ? "radio-button-fill" : "radio-button")
|
Image(optionsAudioRoute == 3 ? "radio-button-fill" : "radio-button")
|
||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
.resizable()
|
.resizable()
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
@ -258,6 +278,97 @@ struct CallView: View {
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func changeLayoutBottomSheet() -> some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Button(action: {
|
||||||
|
optionsChangeLayout = 1
|
||||||
|
changeLayoutSheet = false
|
||||||
|
}, label: {
|
||||||
|
HStack {
|
||||||
|
Image(optionsChangeLayout == 1 ? "radio-button-fill" : "radio-button")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(callViewModel.participantList.count > 5 ? Color.gray500 : .white)
|
||||||
|
.frame(width: 25, height: 25, alignment: .leading)
|
||||||
|
.padding(.all, 10)
|
||||||
|
|
||||||
|
Text("Mosaïque")
|
||||||
|
.foregroundStyle(callViewModel.participantList.count > 5 ? Color.gray500 : .white)
|
||||||
|
.default_text_style_white(styleSize: 15)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("squares-four")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(callViewModel.participantList.count > 5 ? Color.gray500 : .white)
|
||||||
|
.frame(width: 25, height: 25, alignment: .leading)
|
||||||
|
.padding(.all, 10)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.disabled(callViewModel.participantList.count > 5)
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
optionsChangeLayout = 2
|
||||||
|
changeLayoutSheet = false
|
||||||
|
}, label: {
|
||||||
|
HStack {
|
||||||
|
Image(optionsChangeLayout == 2 ? "radio-button-fill" : "radio-button")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 25, height: 25, alignment: .leading)
|
||||||
|
.padding(.all, 10)
|
||||||
|
|
||||||
|
Text("Participant actif")
|
||||||
|
.default_text_style_white(styleSize: 15)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("picture-in-picture")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 25, height: 25, alignment: .leading)
|
||||||
|
.padding(.all, 10)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
optionsChangeLayout = 3
|
||||||
|
changeLayoutSheet = false
|
||||||
|
}, label: {
|
||||||
|
HStack {
|
||||||
|
Image(optionsChangeLayout == 3 ? "radio-button-fill" : "radio-button")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 25, height: 25, alignment: .leading)
|
||||||
|
.padding(.all, 10)
|
||||||
|
|
||||||
|
Text("Audio seulement")
|
||||||
|
.default_text_style_white(styleSize: 15)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("waveform")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 25, height: 25, alignment: .leading)
|
||||||
|
.padding(.all, 10)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.background(Color.gray600)
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
// swiftlint:disable:next cyclomatic_complexity
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
func innerView(geometry: GeometryProxy) -> some View {
|
func innerView(geometry: GeometryProxy) -> some View {
|
||||||
|
|
@ -563,6 +674,153 @@ struct CallView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.activeSpeakerParticipant != nil {
|
} else if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.activeSpeakerParticipant != nil {
|
||||||
|
if optionsChangeLayout == 1 && callViewModel.participantList.count <= 5 {
|
||||||
|
mosaicMode(geometry: geometry, height: (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom))
|
||||||
|
} else {
|
||||||
|
activeSpeakerMode(geometry: geometry)
|
||||||
|
}
|
||||||
|
} else if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.participantList.isEmpty {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("En attente d'autres participants...")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_300(styleSize: 25)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
UIPasteboard.general.setValue(
|
||||||
|
callViewModel.remoteAddressString,
|
||||||
|
forPasteboardType: UTType.plainText.identifier
|
||||||
|
)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
|
||||||
|
ToastViewModel.shared.displayToast = true
|
||||||
|
}
|
||||||
|
}, label: {
|
||||||
|
HStack {
|
||||||
|
Image("share-network")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(Color.grayMain2c400)
|
||||||
|
.frame(width: 30, height: 30)
|
||||||
|
|
||||||
|
Text("Partager le lien")
|
||||||
|
.foregroundStyle(Color.grayMain2c400)
|
||||||
|
.default_text_style(styleSize: 25)
|
||||||
|
.frame(height: 40)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.cornerRadius(60)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 60)
|
||||||
|
.inset(by: 0.5)
|
||||||
|
.stroke(Color.grayMain2c400, lineWidth: 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
LinphoneVideoViewHolder { view in
|
||||||
|
coreContext.doOnCoreQueue { core in
|
||||||
|
core.nativePreviewWindow = view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(10)
|
||||||
|
.padding(.trailing, abs(angleDegree/2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(
|
||||||
|
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
|
||||||
|
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if callViewModel.isRecording {
|
||||||
|
HStack {
|
||||||
|
VStack {
|
||||||
|
Image("record-fill")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(Color.redDanger500)
|
||||||
|
.frame(width: 32, height: 32)
|
||||||
|
.padding(10)
|
||||||
|
.if(fullscreenVideo && !telecomManager.isPausedByRemote) { view in
|
||||||
|
view.padding(.top, 30)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(
|
||||||
|
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
|
||||||
|
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(
|
||||||
|
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
|
||||||
|
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
|
||||||
|
)
|
||||||
|
.background(Color.gray900)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.horizontal, fullscreenVideo && !telecomManager.isPausedByRemote ? 0 : 4)
|
||||||
|
.onRotate { newOrientation in
|
||||||
|
let oldOrientation = orientation
|
||||||
|
orientation = newOrientation
|
||||||
|
if orientation == .portrait || orientation == .portraitUpsideDown {
|
||||||
|
angleDegree = 0
|
||||||
|
} else {
|
||||||
|
if orientation == .landscapeLeft {
|
||||||
|
angleDegree = -90
|
||||||
|
} else if orientation == .landscapeRight {
|
||||||
|
angleDegree = 90
|
||||||
|
} else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
|
||||||
|
angleDegree = 90
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldOrientation != orientation && oldOrientation != .faceUp) || (oldOrientation == .faceUp && (orientation == .landscapeLeft || orientation == .landscapeRight)) {
|
||||||
|
telecomManager.callStarted = false
|
||||||
|
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
telecomManager.callStarted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callViewModel.orientationUpdate(orientation: orientation)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if orientation == .portrait && orientation == .portraitUpsideDown {
|
||||||
|
angleDegree = 0
|
||||||
|
} else {
|
||||||
|
if orientation == .landscapeLeft {
|
||||||
|
angleDegree = -90
|
||||||
|
} else if orientation == .landscapeRight {
|
||||||
|
angleDegree = 90
|
||||||
|
} else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
|
||||||
|
angleDegree = 90
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callViewModel.orientationUpdate(orientation: orientation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// swiftlint:enable function_body_length
|
||||||
|
|
||||||
|
func activeSpeakerMode(geometry: GeometryProxy) -> some View {
|
||||||
|
ZStack {
|
||||||
if callViewModel.activeSpeakerParticipant!.onPause {
|
if callViewModel.activeSpeakerParticipant!.onPause {
|
||||||
VStack {
|
VStack {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
@ -781,6 +1039,10 @@ struct CallView: View {
|
||||||
}
|
}
|
||||||
.frame(width: 140, height: 140)
|
.frame(width: 140, height: 140)
|
||||||
.background(Color.gray600)
|
.background(Color.gray600)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 20)
|
||||||
|
.stroke(callViewModel.myParticipantModel != nil && callViewModel.myParticipantModel!.isSpeaking ? .white : Color.gray600, lineWidth: 4)
|
||||||
|
)
|
||||||
.cornerRadius(20)
|
.cornerRadius(20)
|
||||||
|
|
||||||
ForEach(0..<callViewModel.participantList.count, id: \.self) { index in
|
ForEach(0..<callViewModel.participantList.count, id: \.self) { index in
|
||||||
|
|
@ -880,6 +1142,10 @@ struct CallView: View {
|
||||||
}
|
}
|
||||||
.frame(width: 140, height: 140)
|
.frame(width: 140, height: 140)
|
||||||
.background(Color.gray600)
|
.background(Color.gray600)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 20)
|
||||||
|
.stroke(callViewModel.participantList[index].isSpeaking ? .white : Color.gray600, lineWidth: 4)
|
||||||
|
)
|
||||||
.cornerRadius(20)
|
.cornerRadius(20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -894,145 +1160,490 @@ struct CallView: View {
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
.padding(.leading, -10)
|
.padding(.leading, -10)
|
||||||
}
|
}
|
||||||
} else if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.participantList.isEmpty {
|
}
|
||||||
|
.onAppear {
|
||||||
|
optionsChangeLayout = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
|
func mosaicMode(geometry: GeometryProxy, height: Double) -> some View {
|
||||||
|
VStack {
|
||||||
|
if geometry.size.width < geometry.size.height {
|
||||||
|
let maxValue = max(
|
||||||
|
((geometry.size.width/2) - 10.0) * ceil(Double(callViewModel.participantList.count + 1) / 2.0) > height ? ((height / 3) - 10.0) : ((geometry.size.width/2) - 10.0),
|
||||||
|
((height / Double(callViewModel.participantList.count + 1)) - 10.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyVGrid(columns: [
|
||||||
|
GridItem(.adaptive(
|
||||||
|
minimum: maxValue
|
||||||
|
))
|
||||||
|
], spacing: 10) {
|
||||||
|
if callViewModel.myParticipantModel != nil {
|
||||||
|
ZStack {
|
||||||
|
if callViewModel.myParticipantModel!.isJoining {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text("En attente d'autres participants...")
|
ActivityIndicator(color: .white)
|
||||||
|
.frame(width: maxValue/4, height: maxValue/4)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
|
||||||
|
Text("Joining...")
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
.default_text_style_300(styleSize: 25)
|
.default_text_style_500(styleSize: 14)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.padding(.bottom, 4)
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
UIPasteboard.general.setValue(
|
|
||||||
callViewModel.remoteAddressString,
|
|
||||||
forPasteboardType: UTType.plainText.identifier
|
|
||||||
)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
|
|
||||||
ToastViewModel.shared.displayToast = true
|
|
||||||
}
|
|
||||||
}, label: {
|
|
||||||
HStack {
|
|
||||||
Image("share-network")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.resizable()
|
|
||||||
.foregroundStyle(Color.grayMain2c400)
|
|
||||||
.frame(width: 30, height: 30)
|
|
||||||
|
|
||||||
Text("Partager le lien")
|
|
||||||
.foregroundStyle(Color.grayMain2c400)
|
|
||||||
.default_text_style(styleSize: 25)
|
|
||||||
.frame(height: 40)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
.cornerRadius(60)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 60)
|
|
||||||
.inset(by: 0.5)
|
|
||||||
.stroke(Color.grayMain2c400, lineWidth: 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
} else if callViewModel.myParticipantModel!.onPause {
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
Image("pause")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: maxValue/4, height: maxValue/4)
|
||||||
|
|
||||||
|
Text("En pause")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if callViewModel.myParticipantModel != nil {
|
||||||
|
Avatar(contactAvatarModel: callViewModel.myParticipantModel!.avatarModel, avatarSize: maxValue/2, hidePresence: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(width: maxValue, height: maxValue)
|
||||||
|
|
||||||
|
if callViewModel.videoDisplayed {
|
||||||
LinphoneVideoViewHolder { view in
|
LinphoneVideoViewHolder { view in
|
||||||
coreContext.doOnCoreQueue { core in
|
coreContext.doOnCoreQueue { core in
|
||||||
core.nativePreviewWindow = view
|
core.nativePreviewWindow = view
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(10)
|
|
||||||
.padding(.trailing, abs(angleDegree/2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(
|
.frame(
|
||||||
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
|
width: 120 * ceil(maxValue / 120),
|
||||||
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
|
height: 160 * ceil(maxValue / 120)
|
||||||
)
|
)
|
||||||
|
.scaledToFill()
|
||||||
|
.clipped()
|
||||||
}
|
}
|
||||||
|
|
||||||
if callViewModel.isRecording {
|
if callViewModel.myParticipantModel!.isMuted {
|
||||||
HStack {
|
|
||||||
VStack {
|
VStack {
|
||||||
Image("record-fill")
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Image("microphone-slash")
|
||||||
.renderingMode(.template)
|
.renderingMode(.template)
|
||||||
.resizable()
|
.resizable()
|
||||||
.foregroundStyle(Color.redDanger500)
|
.foregroundStyle(Color.grayMain2c800)
|
||||||
.frame(width: 32, height: 32)
|
.frame(width: 12, height: 12)
|
||||||
.padding(10)
|
}
|
||||||
.if(fullscreenVideo && !telecomManager.isPausedByRemote) { view in
|
.padding(2)
|
||||||
view.padding(.top, 30)
|
.background(.white)
|
||||||
|
.cornerRadius(40)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.all, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
if callViewModel.myParticipantModel != nil {
|
||||||
|
Text(callViewModel.myParticipantModel!.name)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.bottom, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: maxValue, height: maxValue)
|
||||||
}
|
}
|
||||||
.frame(
|
.frame(
|
||||||
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
|
width: maxValue,
|
||||||
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
|
height: maxValue,
|
||||||
|
alignment: .center
|
||||||
)
|
)
|
||||||
}
|
.background(Color.gray600)
|
||||||
}
|
.overlay(
|
||||||
.frame(
|
RoundedRectangle(cornerRadius: 20)
|
||||||
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
|
.stroke(callViewModel.myParticipantModel!.isSpeaking ? .white : Color.gray600, lineWidth: 4)
|
||||||
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
|
|
||||||
)
|
)
|
||||||
.background(Color.gray900)
|
|
||||||
.cornerRadius(20)
|
.cornerRadius(20)
|
||||||
.padding(.horizontal, fullscreenVideo && !telecomManager.isPausedByRemote ? 0 : 4)
|
}
|
||||||
.onRotate { newOrientation in
|
|
||||||
let oldOrientation = orientation
|
ForEach(0..<callViewModel.participantList.count, id: \.self) { index in
|
||||||
orientation = newOrientation
|
if index < callViewModel.participantList.count {
|
||||||
if orientation == .portrait || orientation == .portraitUpsideDown {
|
ZStack {
|
||||||
angleDegree = 0
|
if callViewModel.participantList[index].isJoining {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
ActivityIndicator(color: .white)
|
||||||
|
.frame(width: maxValue/4, height: maxValue/4)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
|
||||||
|
Text("Joining...")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else if callViewModel.participantList[index].onPause {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("pause")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: maxValue/4, height: maxValue/4)
|
||||||
|
|
||||||
|
Text("En pause")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if orientation == .landscapeLeft {
|
VStack {
|
||||||
angleDegree = -90
|
Spacer()
|
||||||
} else if orientation == .landscapeRight {
|
|
||||||
angleDegree = 90
|
Avatar(contactAvatarModel: callViewModel.participantList[index].avatarModel, avatarSize: maxValue/2, hidePresence: true)
|
||||||
} else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
|
|
||||||
angleDegree = 90
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(width: maxValue, height: maxValue)
|
||||||
|
|
||||||
|
LinphoneVideoViewHolder { view in
|
||||||
|
coreContext.doOnCoreQueue { core in
|
||||||
|
if index < callViewModel.participantList.count {
|
||||||
|
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
|
||||||
|
if participantVideo != nil && participantVideo!.devices.first != nil {
|
||||||
|
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldOrientation != orientation && oldOrientation != .faceUp) || (oldOrientation == .faceUp && (orientation == .landscapeLeft || orientation == .landscapeRight)) {
|
if callViewModel.participantList[index].isMuted {
|
||||||
telecomManager.callStarted = false
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
HStack(alignment: .center) {
|
||||||
telecomManager.callStarted = true
|
Image("microphone-slash")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(Color.grayMain2c800)
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
}
|
||||||
|
.padding(2)
|
||||||
|
.background(.white)
|
||||||
|
.cornerRadius(40)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.all, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callViewModel.orientationUpdate(orientation: orientation)
|
VStack(alignment: .leading) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(callViewModel.participantList[index].name)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.bottom, 6)
|
||||||
|
}
|
||||||
|
.frame(width: maxValue, height: maxValue)
|
||||||
|
}
|
||||||
|
.frame(
|
||||||
|
width: maxValue,
|
||||||
|
height: maxValue,
|
||||||
|
alignment: .center
|
||||||
|
)
|
||||||
|
.background(Color.gray600)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 20)
|
||||||
|
.stroke(callViewModel.participantList[index].isSpeaking ? .white : Color.gray600, lineWidth: 4)
|
||||||
|
)
|
||||||
|
.cornerRadius(20)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
if orientation == .portrait && orientation == .portraitUpsideDown {
|
|
||||||
angleDegree = 0
|
|
||||||
} else {
|
} else {
|
||||||
if orientation == .landscapeLeft {
|
let maxValue = max(
|
||||||
angleDegree = -90
|
((geometry.size.width/3) - 10.0) * ceil(Double(callViewModel.participantList.count + 1) / 3.0) > height ? ((height / 2) - 10.0) : ((geometry.size.width/3) - 10.0),
|
||||||
} else if orientation == .landscapeRight {
|
((geometry.size.width/Double(callViewModel.participantList.count + 1)) - 10.0) > height ? height - 20 : ((geometry.size.width/Double(callViewModel.participantList.count + 1)) - 10.0)
|
||||||
angleDegree = 90
|
)
|
||||||
} else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
|
|
||||||
angleDegree = 90
|
LazyHGrid(rows: [
|
||||||
|
GridItem(.adaptive(
|
||||||
|
minimum: maxValue
|
||||||
|
))
|
||||||
|
], spacing: 10) {
|
||||||
|
if callViewModel.myParticipantModel != nil {
|
||||||
|
ZStack {
|
||||||
|
if callViewModel.myParticipantModel!.isJoining {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
ActivityIndicator(color: .white)
|
||||||
|
.frame(width: maxValue/4, height: maxValue/4)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
|
||||||
|
Text("Joining...")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else if callViewModel.myParticipantModel!.onPause {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("pause")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: maxValue/4, height: maxValue/4)
|
||||||
|
|
||||||
|
Text("En pause")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if callViewModel.myParticipantModel != nil {
|
||||||
|
Avatar(contactAvatarModel: callViewModel.myParticipantModel!.avatarModel, avatarSize: maxValue/2, hidePresence: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(width: maxValue, height: maxValue)
|
||||||
|
|
||||||
|
if callViewModel.videoDisplayed {
|
||||||
|
LinphoneVideoViewHolder { view in
|
||||||
|
coreContext.doOnCoreQueue { core in
|
||||||
|
core.nativePreviewWindow = view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(
|
||||||
|
width: 160 * ceil(maxValue / 120),
|
||||||
|
height: 120 * ceil(maxValue / 120)
|
||||||
|
)
|
||||||
|
.scaledToFill()
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
|
||||||
|
if callViewModel.myParticipantModel!.isMuted {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Image("microphone-slash")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(Color.grayMain2c800)
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
}
|
||||||
|
.padding(2)
|
||||||
|
.background(.white)
|
||||||
|
.cornerRadius(40)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.all, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callViewModel.orientationUpdate(orientation: orientation)
|
VStack(alignment: .leading) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if callViewModel.myParticipantModel != nil {
|
||||||
|
Text(callViewModel.myParticipantModel!.name)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.bottom, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: maxValue, height: maxValue)
|
||||||
|
}
|
||||||
|
.frame(
|
||||||
|
width: maxValue,
|
||||||
|
height: maxValue,
|
||||||
|
alignment: .center
|
||||||
|
)
|
||||||
|
.background(Color.gray600)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 20)
|
||||||
|
.stroke(callViewModel.myParticipantModel!.isSpeaking ? .white : Color.gray600, lineWidth: 4)
|
||||||
|
)
|
||||||
|
.cornerRadius(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(0..<callViewModel.participantList.count, id: \.self) { index in
|
||||||
|
if index < callViewModel.participantList.count {
|
||||||
|
ZStack {
|
||||||
|
if callViewModel.participantList[index].isJoining {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
ActivityIndicator(color: .white)
|
||||||
|
.frame(width: maxValue/4, height: maxValue/4)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
|
||||||
|
Text("Joining...")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else if callViewModel.participantList[index].onPause {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("pause")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: maxValue/4, height: maxValue/4)
|
||||||
|
|
||||||
|
Text("En pause")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Avatar(contactAvatarModel: callViewModel.participantList[index].avatarModel, avatarSize: maxValue/2, hidePresence: true)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(width: maxValue, height: maxValue)
|
||||||
|
|
||||||
|
LinphoneVideoViewHolder { view in
|
||||||
|
coreContext.doOnCoreQueue { core in
|
||||||
|
if index < callViewModel.participantList.count {
|
||||||
|
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
|
||||||
|
if participantVideo != nil && participantVideo!.devices.first != nil {
|
||||||
|
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if callViewModel.participantList[index].isMuted {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Image("microphone-slash")
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.foregroundStyle(Color.grayMain2c800)
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
}
|
||||||
|
.padding(2)
|
||||||
|
.background(.white)
|
||||||
|
.cornerRadius(40)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.all, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(callViewModel.participantList[index].name)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.default_text_style_500(styleSize: 14)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.bottom, 6)
|
||||||
|
}
|
||||||
|
.frame(width: maxValue, height: maxValue)
|
||||||
|
}
|
||||||
|
.frame(
|
||||||
|
width: maxValue,
|
||||||
|
height: maxValue,
|
||||||
|
alignment: .center
|
||||||
|
)
|
||||||
|
.background(Color.gray600)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 20)
|
||||||
|
.stroke(callViewModel.participantList[index].isSpeaking ? .white : Color.gray600, lineWidth: 4)
|
||||||
|
)
|
||||||
|
.cornerRadius(20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// swiftlint:enable function_body_length
|
|
||||||
|
|
||||||
// swiftlint:disable function_body_length
|
// swiftlint:disable function_body_length
|
||||||
func bottomSheetContent(geo: GeometryProxy) -> some View {
|
func bottomSheetContent(geo: GeometryProxy) -> some View {
|
||||||
|
|
@ -1326,6 +1937,7 @@ struct CallView: View {
|
||||||
} else {
|
} else {
|
||||||
VStack {
|
VStack {
|
||||||
Button {
|
Button {
|
||||||
|
changeLayoutSheet = true
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Image("notebook")
|
Image("notebook")
|
||||||
|
|
@ -1651,6 +2263,7 @@ struct CallView: View {
|
||||||
} else {
|
} else {
|
||||||
VStack {
|
VStack {
|
||||||
Button {
|
Button {
|
||||||
|
changeLayoutSheet = true
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Image("notebook")
|
Image("notebook")
|
||||||
|
|
@ -1894,3 +2507,4 @@ struct PressedButtonStyle: ButtonStyle {
|
||||||
}
|
}
|
||||||
// swiftlint:enable type_body_length
|
// swiftlint:enable type_body_length
|
||||||
// swiftlint:enable line_length
|
// swiftlint:enable line_length
|
||||||
|
// swiftlint:enable file_length
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,27 @@ struct ParticipantsListFragment: View {
|
||||||
.background(.white)
|
.background(.white)
|
||||||
|
|
||||||
participantsList
|
participantsList
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
NavigationLink(destination: {
|
||||||
|
//AddParticipantsFragment()
|
||||||
|
}, label: {
|
||||||
|
Image("plus")
|
||||||
|
.resizable()
|
||||||
|
.renderingMode(.template)
|
||||||
|
.frame(width: 25, height: 25)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.padding()
|
||||||
|
.background(Color.orangeMain500)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.shadow(color: .black.opacity(0.2), radius: 4)
|
||||||
|
|
||||||
|
})
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.padding(.trailing, 10)
|
||||||
}
|
}
|
||||||
.background(.white)
|
.background(.white)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,9 @@ class ParticipantModel: ObservableObject {
|
||||||
@Published var onPause: Bool
|
@Published var onPause: Bool
|
||||||
@Published var isMuted: Bool
|
@Published var isMuted: Bool
|
||||||
@Published var isAdmin: Bool
|
@Published var isAdmin: Bool
|
||||||
|
@Published var isSpeaking: Bool
|
||||||
|
|
||||||
init(address: Address, isJoining: Bool = false, onPause: Bool = false, isMuted: Bool = false, isAdmin: Bool = false) {
|
init(address: Address, isJoining: Bool = false, onPause: Bool = false, isMuted: Bool = false, isAdmin: Bool = false, isSpeaking: Bool = false) {
|
||||||
self.address = address
|
self.address = address
|
||||||
|
|
||||||
self.sipUri = address.asStringUriOnly()
|
self.sipUri = address.asStringUriOnly()
|
||||||
|
|
@ -50,5 +51,6 @@ class ParticipantModel: ObservableObject {
|
||||||
self.onPause = onPause
|
self.onPause = onPause
|
||||||
self.isMuted = isMuted
|
self.isMuted = isMuted
|
||||||
self.isAdmin = isAdmin
|
self.isAdmin = isAdmin
|
||||||
|
self.isSpeaking = isSpeaking
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,7 @@ class CallViewModel: ObservableObject {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
func addConferenceCallBacks() {
|
func addConferenceCallBacks() {
|
||||||
coreContext.doOnCoreQueue { core in
|
coreContext.doOnCoreQueue { core in
|
||||||
self.mConferenceSuscriptions.insert(
|
self.mConferenceSuscriptions.insert(
|
||||||
|
|
@ -386,7 +387,52 @@ class CallViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var activeSpeakerParticipantTmp: ParticipantModel? = nil
|
||||||
|
var activeSpeakerNameTmp = ""
|
||||||
|
|
||||||
|
if self.activeSpeakerParticipant == nil {
|
||||||
|
if cbValue.conference.activeSpeakerParticipantDevice?.address != nil {
|
||||||
|
activeSpeakerParticipantTmp = ParticipantModel(
|
||||||
|
address: cbValue.conference.activeSpeakerParticipantDevice!.address!,
|
||||||
|
isJoining: false,
|
||||||
|
onPause: cbValue.conference.activeSpeakerParticipantDevice!.state == .OnHold,
|
||||||
|
isMuted: cbValue.conference.activeSpeakerParticipantDevice!.isMuted
|
||||||
|
)
|
||||||
|
} else if cbValue.conference.participantList.first?.address != nil && cbValue.conference.participantList.first!.address!.clone()!.equal(address2: (cbValue.conference.me?.address)!) {
|
||||||
|
activeSpeakerParticipantTmp = ParticipantModel(
|
||||||
|
address: cbValue.conference.participantDeviceList.first!.address!,
|
||||||
|
isJoining: false,
|
||||||
|
onPause: cbValue.conference.participantDeviceList.first!.state == .OnHold,
|
||||||
|
isMuted: cbValue.conference.participantDeviceList.first!.isMuted
|
||||||
|
)
|
||||||
|
} else if cbValue.conference.participantList.last?.address != nil {
|
||||||
|
activeSpeakerParticipantTmp = ParticipantModel(
|
||||||
|
address: cbValue.conference.participantDeviceList.last!.address!,
|
||||||
|
isJoining: false,
|
||||||
|
onPause: cbValue.conference.participantDeviceList.last!.state == .OnHold,
|
||||||
|
isMuted: cbValue.conference.participantDeviceList.last!.isMuted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if activeSpeakerParticipantTmp != nil {
|
||||||
|
let friend = ContactsManager.shared.getFriendWithAddress(address: activeSpeakerParticipantTmp!.address)
|
||||||
|
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
|
||||||
|
activeSpeakerNameTmp = friend!.address!.displayName!
|
||||||
|
} else {
|
||||||
|
if activeSpeakerParticipantTmp!.address.displayName != nil {
|
||||||
|
activeSpeakerNameTmp = activeSpeakerParticipantTmp!.address.displayName!
|
||||||
|
} else if activeSpeakerParticipantTmp!.address.username != nil {
|
||||||
|
activeSpeakerNameTmp = activeSpeakerParticipantTmp!.address.username!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
if self.activeSpeakerParticipant == nil {
|
||||||
|
self.activeSpeakerParticipant = activeSpeakerParticipantTmp
|
||||||
|
self.activeSpeakerName = activeSpeakerNameTmp
|
||||||
|
}
|
||||||
self.participantList = participantListTmp
|
self.participantList = participantListTmp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -435,7 +481,7 @@ class CallViewModel: ObservableObject {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.activeSpeakerParticipant!.isMuted = isMutedTmp
|
self.activeSpeakerParticipant!.isMuted = isMutedTmp
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
self.participantList.forEach({ participantDevice in
|
self.participantList.forEach({ participantDevice in
|
||||||
if participantDevice.address.equal(address2: cbValue.participantDevice.address!) {
|
if participantDevice.address.equal(address2: cbValue.participantDevice.address!) {
|
||||||
let isMutedTmp = cbValue.isMuted
|
let isMutedTmp = cbValue.isMuted
|
||||||
|
|
@ -446,7 +492,6 @@ class CallViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.mConferenceSuscriptions.insert(
|
self.mConferenceSuscriptions.insert(
|
||||||
|
|
@ -461,7 +506,7 @@ class CallViewModel: ObservableObject {
|
||||||
self.activeSpeakerParticipant!.onPause = activeSpeakerParticipantOnPauseTmp
|
self.activeSpeakerParticipant!.onPause = activeSpeakerParticipantOnPauseTmp
|
||||||
self.activeSpeakerParticipant!.isJoining = activeSpeakerParticipantIsJoiningTmp
|
self.activeSpeakerParticipant!.isJoining = activeSpeakerParticipantIsJoiningTmp
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
self.participantList.forEach({ participantDevice in
|
self.participantList.forEach({ participantDevice in
|
||||||
if participantDevice.address.equal(address2: cbValue.device.address!) {
|
if participantDevice.address.equal(address2: cbValue.device.address!) {
|
||||||
let participantDeviceOnPauseTmp = cbValue.state == .OnHold
|
let participantDeviceOnPauseTmp = cbValue.state == .OnHold
|
||||||
|
|
@ -473,7 +518,6 @@ class CallViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.mConferenceSuscriptions.insert(
|
self.mConferenceSuscriptions.insert(
|
||||||
|
|
@ -483,7 +527,7 @@ class CallViewModel: ObservableObject {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.myParticipantModel!.isAdmin = isAdmin
|
self.myParticipantModel!.isAdmin = isAdmin
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
self.participantList.forEach({ participantDevice in
|
self.participantList.forEach({ participantDevice in
|
||||||
if participantDevice.address.clone()!.equal(address2: cbValue.participant.address!) {
|
if participantDevice.address.clone()!.equal(address2: cbValue.participant.address!) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
@ -492,6 +536,23 @@ class CallViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.mConferenceSuscriptions.insert(
|
||||||
|
self.currentCall?.conference?.publisher?.onParticipantDeviceIsSpeakingChanged?.postOnMainQueue {(cbValue: (conference: Conference, participantDevice: ParticipantDevice, isSpeaking: Bool)) in
|
||||||
|
let isSpeaking = cbValue.participantDevice.isSpeaking
|
||||||
|
if self.myParticipantModel != nil && self.myParticipantModel!.address.clone()!.equal(address2: cbValue.participantDevice.address!) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.myParticipantModel!.isSpeaking = isSpeaking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.participantList.forEach({ participantDeviceList in
|
||||||
|
if participantDeviceList.address.clone()!.equal(address2: cbValue.participantDevice.address!) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
participantDeviceList.isSpeaking = isSpeaking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue