Add mosaic mode to conference call view

This commit is contained in:
Benoit Martins 2024-04-29 16:03:40 +02:00
parent b16372c420
commit 1f0c3fa5f7
13 changed files with 1165 additions and 370 deletions

View 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
}
}

View 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

View 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
}
}

View 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

View 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
}
}

View 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

View 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
}
}

View 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

View file

@ -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" : {

View file

@ -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

View file

@ -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)

View file

@ -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
} }
} }

View file

@ -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
}
}
})
} }
) )
} }