forked from mirrors/linphone-iphone
Start group call
This commit is contained in:
parent
9befe0695a
commit
b5d98cc45a
9 changed files with 557 additions and 183 deletions
|
|
@ -59,6 +59,7 @@
|
|||
D714035B2BE11E00004BD8CA /* CallMediaEncryptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D714035A2BE11E00004BD8CA /* CallMediaEncryptionModel.swift */; };
|
||||
D714DE602C1B3B34006C1F1D /* RegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D714DE5F2C1B3B34006C1F1D /* RegisterViewModel.swift */; };
|
||||
D714DE622C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D714DE612C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift */; };
|
||||
D71556362C297DB1009A8CEF /* StartGroupCallFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71556352C297DB1009A8CEF /* StartGroupCallFragment.swift */; };
|
||||
D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071D2AC5922E0037746F /* ColorExtension.swift */; };
|
||||
D71707202AC5989C0037746F /* TextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071F2AC5989C0037746F /* TextExtension.swift */; };
|
||||
D7173EBE2B7A5C0A00BCC481 /* LinphoneUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */; };
|
||||
|
|
@ -233,6 +234,7 @@
|
|||
D714035A2BE11E00004BD8CA /* CallMediaEncryptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMediaEncryptionModel.swift; sourceTree = "<group>"; };
|
||||
D714DE5F2C1B3B34006C1F1D /* RegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterViewModel.swift; sourceTree = "<group>"; };
|
||||
D714DE612C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterCodeConfirmationFragment.swift; sourceTree = "<group>"; };
|
||||
D71556352C297DB1009A8CEF /* StartGroupCallFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartGroupCallFragment.swift; sourceTree = "<group>"; };
|
||||
D717071D2AC5922E0037746F /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = "<group>"; };
|
||||
D717071F2AC5989C0037746F /* TextExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextExtension.swift; sourceTree = "<group>"; };
|
||||
D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinphoneUtils.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -624,6 +626,7 @@
|
|||
D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */,
|
||||
D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */,
|
||||
D79622332B1DFE600037EACD /* DialerBottomSheet.swift */,
|
||||
D71556352C297DB1009A8CEF /* StartGroupCallFragment.swift */,
|
||||
);
|
||||
path = Fragments;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1051,6 +1054,7 @@
|
|||
66E50A492BD12B2300AD61CA /* MeetingsView.swift in Sources */,
|
||||
D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */,
|
||||
D717630D2BD7BD0E00464097 /* ParticipantsListFragment.swift in Sources */,
|
||||
D71556362C297DB1009A8CEF /* StartGroupCallFragment.swift in Sources */,
|
||||
C6A5A9452C10B6270070FEA4 /* OIDAuthStateExtension.swift in Sources */,
|
||||
D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */,
|
||||
D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -461,12 +461,45 @@
|
|||
},
|
||||
"Conditions de service" : {
|
||||
|
||||
},
|
||||
"conference_failed_to_create_group_call_toast" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Failed to create a group call!"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "L'appel de groupe n'a pas pu être créé!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Configuration failed" : {
|
||||
|
||||
},
|
||||
"Configuration successfully applied" : {
|
||||
|
||||
},
|
||||
"Confirm" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Confirm"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Confirmer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Connexion à la réunion" : {
|
||||
|
||||
|
|
@ -758,6 +791,54 @@
|
|||
},
|
||||
"History has been deleted" : {
|
||||
|
||||
},
|
||||
"history_call_start_create_group_call" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Create a group call"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Démarrer un appel de groupe"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"history_group_call_start_dialog_set_subject" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Set group call subject"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nommer l'appel de groupe"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"history_group_call_start_dialog_subject_hint" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Group call subject"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nom de l'appel de groupe"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"I prefere create an account" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -570,12 +570,6 @@ struct ScrollOffsetPreferenceKey: PreferenceKey {
|
|||
}
|
||||
}
|
||||
|
||||
extension UIApplication {
|
||||
func endEditing() {
|
||||
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct ImagePicker: UIViewControllerRepresentable {
|
||||
@ObservedObject var conversationViewModel: ConversationViewModel
|
||||
@Binding var selectedMedia: [Attachment]
|
||||
|
|
|
|||
|
|
@ -192,7 +192,15 @@ struct ToastView: View {
|
|||
.foregroundStyle(Color.redDanger500)
|
||||
.default_text_style(styleSize: 15)
|
||||
.padding(8)
|
||||
|
||||
|
||||
|
||||
case "Failed_to_create_group_call_error":
|
||||
Text("conference_failed_to_create_group_call_toast")
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(Color.redDanger500)
|
||||
.default_text_style(styleSize: 15)
|
||||
.padding(8)
|
||||
|
||||
default:
|
||||
Text("Error")
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import linphonesw
|
|||
|
||||
struct StartCallFragment: View {
|
||||
|
||||
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
|
||||
|
||||
@ObservedObject var contactsManager = ContactsManager.shared
|
||||
@ObservedObject var magicSearch = MagicSearchSingleton.shared
|
||||
@ObservedObject private var telecomManager = TelecomManager.shared
|
||||
|
|
@ -35,213 +37,271 @@ struct StartCallFragment: View {
|
|||
@FocusState var isSearchFieldFocused: Bool
|
||||
@State private var delayedColor = Color.white
|
||||
|
||||
@FocusState var isMessageTextFocused: Bool
|
||||
|
||||
var resetCallView: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack(spacing: 1) {
|
||||
|
||||
Rectangle()
|
||||
.foregroundColor(delayedColor)
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.frame(height: 0)
|
||||
.task(delayColor)
|
||||
|
||||
HStack {
|
||||
Image("caret-left")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 2)
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
magicSearch.searchForContacts(
|
||||
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
|
||||
if callViewModel.isTransferInsteadCall == true {
|
||||
callViewModel.isTransferInsteadCall = false
|
||||
NavigationView {
|
||||
ZStack {
|
||||
VStack(spacing: 1) {
|
||||
|
||||
Rectangle()
|
||||
.foregroundColor(delayedColor)
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.frame(height: 0)
|
||||
.task(delayColor)
|
||||
|
||||
HStack {
|
||||
Image("caret-left")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 2)
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
magicSearch.searchForContacts(
|
||||
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
|
||||
if callViewModel.isTransferInsteadCall == true {
|
||||
callViewModel.isTransferInsteadCall = false
|
||||
}
|
||||
|
||||
resetCallView()
|
||||
}
|
||||
|
||||
resetCallView()
|
||||
startCallViewModel.searchField = ""
|
||||
magicSearch.currentFilterSuggestions = ""
|
||||
delayColorDismiss()
|
||||
withAnimation {
|
||||
isShowStartCallFragment.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
Text(!callViewModel.isTransferInsteadCall ? "New call" : "Transfer call to")
|
||||
.multilineTextAlignment(.leading)
|
||||
.default_text_style_orange_800(styleSize: 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ZStack(alignment: .trailing) {
|
||||
TextField("Search contact or history call", text: $startCallViewModel.searchField)
|
||||
.default_text_style(styleSize: 15)
|
||||
.frame(height: 25)
|
||||
.focused($isSearchFieldFocused)
|
||||
.padding(.horizontal, 30)
|
||||
.onChange(of: startCallViewModel.searchField) { newValue in
|
||||
magicSearch.currentFilterSuggestions = newValue
|
||||
magicSearch.searchForSuggestions()
|
||||
}
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
showingDialer = false
|
||||
})
|
||||
|
||||
startCallViewModel.searchField = ""
|
||||
magicSearch.currentFilterSuggestions = ""
|
||||
delayColorDismiss()
|
||||
withAnimation {
|
||||
isShowStartCallFragment.toggle()
|
||||
HStack {
|
||||
Button(action: {
|
||||
}, label: {
|
||||
Image("magnifying-glass")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25)
|
||||
})
|
||||
|
||||
Spacer()
|
||||
|
||||
if startCallViewModel.searchField.isEmpty {
|
||||
Button(action: {
|
||||
if !showingDialer {
|
||||
isSearchFieldFocused = false
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
showingDialer = true
|
||||
}
|
||||
} else {
|
||||
showingDialer = false
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
isSearchFieldFocused = true
|
||||
}
|
||||
}
|
||||
}, label: {
|
||||
Image(!showingDialer ? "dialer" : "keyboard")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25)
|
||||
})
|
||||
} else {
|
||||
Button(action: {
|
||||
startCallViewModel.searchField = ""
|
||||
magicSearch.currentFilterSuggestions = ""
|
||||
magicSearch.searchForSuggestions()
|
||||
}, label: {
|
||||
Image("x")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(!callViewModel.isTransferInsteadCall ? "New call" : "Transfer call to")
|
||||
.multilineTextAlignment(.leading)
|
||||
.default_text_style_orange_800(styleSize: 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ZStack(alignment: .trailing) {
|
||||
TextField("Search contact or history call", text: $startCallViewModel.searchField)
|
||||
.default_text_style(styleSize: 15)
|
||||
.frame(height: 25)
|
||||
.focused($isSearchFieldFocused)
|
||||
.padding(.horizontal, 30)
|
||||
.onChange(of: startCallViewModel.searchField) { newValue in
|
||||
magicSearch.currentFilterSuggestions = newValue
|
||||
magicSearch.searchForSuggestions()
|
||||
}
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
showingDialer = false
|
||||
})
|
||||
.padding(.horizontal, 15)
|
||||
.padding(.vertical, 10)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(isSearchFieldFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
|
||||
)
|
||||
.padding(.vertical)
|
||||
.padding(.horizontal)
|
||||
|
||||
HStack {
|
||||
Button(action: {
|
||||
}, label: {
|
||||
Image("magnifying-glass")
|
||||
NavigationLink(destination: {
|
||||
StartGroupCallFragment(startCallViewModel: startCallViewModel)
|
||||
}, label: {
|
||||
HStack {
|
||||
HStack(alignment: .center) {
|
||||
Image("meetings")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 20, height: 20, alignment: .leading)
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.orangeMain500)
|
||||
.cornerRadius(40)
|
||||
|
||||
Text("history_call_start_create_group_call")
|
||||
.foregroundStyle(.black)
|
||||
.default_text_style_800(styleSize: 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("caret-right")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25)
|
||||
})
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
}
|
||||
})
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 20)
|
||||
.background(
|
||||
LinearGradient(gradient: Gradient(colors: [.grayMain2c100, .white]), startPoint: .leading, endPoint: .trailing)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 40)
|
||||
)
|
||||
|
||||
ScrollView {
|
||||
if !ContactsManager.shared.lastSearch.isEmpty {
|
||||
HStack(alignment: .center) {
|
||||
Text("All contacts")
|
||||
.default_text_style_800(styleSize: 16)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if startCallViewModel.searchField.isEmpty {
|
||||
Button(action: {
|
||||
if !showingDialer {
|
||||
isSearchFieldFocused = false
|
||||
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false), startCallFunc: { addr in
|
||||
if callViewModel.isTransferInsteadCall {
|
||||
showingDialer = false
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
magicSearch.searchForContacts(
|
||||
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
showingDialer = true
|
||||
if callViewModel.isTransferInsteadCall == true {
|
||||
callViewModel.isTransferInsteadCall = false
|
||||
}
|
||||
} else {
|
||||
showingDialer = false
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
isSearchFieldFocused = true
|
||||
}
|
||||
resetCallView()
|
||||
}
|
||||
}, label: {
|
||||
Image(!showingDialer ? "dialer" : "keyboard")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25)
|
||||
})
|
||||
} else {
|
||||
Button(action: {
|
||||
|
||||
startCallViewModel.searchField = ""
|
||||
magicSearch.currentFilterSuggestions = ""
|
||||
magicSearch.searchForSuggestions()
|
||||
}, label: {
|
||||
Image("x")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 15)
|
||||
.padding(.vertical, 10)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(isSearchFieldFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
|
||||
)
|
||||
.padding(.vertical)
|
||||
.padding(.horizontal)
|
||||
|
||||
ScrollView {
|
||||
if !ContactsManager.shared.lastSearch.isEmpty {
|
||||
delayColorDismiss()
|
||||
|
||||
withAnimation {
|
||||
isShowStartCallFragment.toggle()
|
||||
callViewModel.blindTransferCallTo(toAddress: addr)
|
||||
}
|
||||
} else {
|
||||
showingDialer = false
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
magicSearch.searchForContacts(
|
||||
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
|
||||
if callViewModel.isTransferInsteadCall == true {
|
||||
callViewModel.isTransferInsteadCall = false
|
||||
}
|
||||
|
||||
resetCallView()
|
||||
}
|
||||
|
||||
startCallViewModel.searchField = ""
|
||||
magicSearch.currentFilterSuggestions = ""
|
||||
delayColorDismiss()
|
||||
|
||||
withAnimation {
|
||||
isShowStartCallFragment.toggle()
|
||||
telecomManager.doCallOrJoinConf(address: addr)
|
||||
}
|
||||
}
|
||||
})
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text("All contacts")
|
||||
Text("Suggestions")
|
||||
.default_text_style_800(styleSize: 16)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false), startCallFunc: { addr in
|
||||
if callViewModel.isTransferInsteadCall {
|
||||
showingDialer = false
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
magicSearch.searchForContacts(
|
||||
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
|
||||
if callViewModel.isTransferInsteadCall == true {
|
||||
callViewModel.isTransferInsteadCall = false
|
||||
}
|
||||
|
||||
resetCallView()
|
||||
}
|
||||
|
||||
startCallViewModel.searchField = ""
|
||||
magicSearch.currentFilterSuggestions = ""
|
||||
delayColorDismiss()
|
||||
|
||||
withAnimation {
|
||||
isShowStartCallFragment.toggle()
|
||||
callViewModel.blindTransferCallTo(toAddress: addr)
|
||||
}
|
||||
} else {
|
||||
showingDialer = false
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
magicSearch.searchForContacts(
|
||||
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
|
||||
if callViewModel.isTransferInsteadCall == true {
|
||||
callViewModel.isTransferInsteadCall = false
|
||||
}
|
||||
|
||||
resetCallView()
|
||||
}
|
||||
|
||||
startCallViewModel.searchField = ""
|
||||
magicSearch.currentFilterSuggestions = ""
|
||||
delayColorDismiss()
|
||||
|
||||
withAnimation {
|
||||
isShowStartCallFragment.toggle()
|
||||
telecomManager.doCallOrJoinConf(address: addr)
|
||||
}
|
||||
}
|
||||
})
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text("Suggestions")
|
||||
.default_text_style_800(styleSize: 16)
|
||||
|
||||
Spacer()
|
||||
suggestionsList
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
suggestionsList
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(.white)
|
||||
|
||||
if !startCallViewModel.participants.isEmpty {
|
||||
startCallPopup
|
||||
.background(.black.opacity(0.65))
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
|
||||
isMessageTextFocused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if startCallViewModel.operationInProgress {
|
||||
PopupLoadingView()
|
||||
.background(.black.opacity(0.65))
|
||||
.onDisappear {
|
||||
isShowStartCallFragment.toggle()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.background(.white)
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
|
||||
@Sendable private func delayColor() async {
|
||||
|
|
@ -343,6 +403,72 @@ struct StartCallFragment: View {
|
|||
.listRowSeparator(.hidden)
|
||||
}
|
||||
}
|
||||
|
||||
var startCallPopup: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack(alignment: .leading) {
|
||||
Text("history_group_call_start_dialog_set_subject")
|
||||
.default_text_style_800(styleSize: 16)
|
||||
.frame(alignment: .leading)
|
||||
.padding(.bottom, 2)
|
||||
|
||||
TextField("history_group_call_start_dialog_subject_hint", text: $startCallViewModel.messageText)
|
||||
.default_text_style(styleSize: 15)
|
||||
.frame(height: 25)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 15)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(isMessageTextFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
|
||||
)
|
||||
.padding(.bottom)
|
||||
.focused($isMessageTextFocused)
|
||||
|
||||
Button(action: {
|
||||
startCallViewModel.participants.removeAll()
|
||||
}, label: {
|
||||
Text("Cancel")
|
||||
.default_text_style_orange_600(styleSize: 20)
|
||||
.frame(height: 35)
|
||||
.frame(maxWidth: .infinity)
|
||||
})
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 10)
|
||||
.cornerRadius(60)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 60)
|
||||
.inset(by: 0.5)
|
||||
.stroke(Color.orangeMain500, lineWidth: 1)
|
||||
)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Button(action: {
|
||||
startCallViewModel.createGroupCall()
|
||||
}, label: {
|
||||
Text("Confirm")
|
||||
.default_text_style_white_600(styleSize: 20)
|
||||
.frame(height: 35)
|
||||
.frame(maxWidth: .infinity)
|
||||
})
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 10)
|
||||
.background(startCallViewModel.messageText.isEmpty ? Color.orangeMain100 : Color.orangeMain500)
|
||||
.cornerRadius(60)
|
||||
.disabled(startCallViewModel.messageText.isEmpty)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 20)
|
||||
.background(.white)
|
||||
.cornerRadius(20)
|
||||
.padding(.horizontal)
|
||||
.frame(maxHeight: .infinity)
|
||||
.shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2)
|
||||
.frame(maxWidth: sharedMainViewModel.maxWidth)
|
||||
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// StartGroupCallFragment.swift
|
||||
// Linphone
|
||||
//
|
||||
// Created by Benoît Martins on 24/06/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StartGroupCallFragment: View {
|
||||
@ObservedObject var startCallViewModel: StartCallViewModel
|
||||
@State var addParticipantsViewModel = AddParticipantsViewModel()
|
||||
|
||||
var body: some View {
|
||||
AddParticipantsFragment(addParticipantsViewModel: addParticipantsViewModel, confirmAddParticipantsFunc: startCallViewModel.addParticipants)
|
||||
.onAppear {
|
||||
addParticipantsViewModel.participantsToAdd = startCallViewModel.participants
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
StartGroupCallFragment(startCallViewModel: StartCallViewModel())
|
||||
}
|
||||
|
|
@ -18,16 +18,147 @@
|
|||
*/
|
||||
|
||||
import linphonesw
|
||||
import Combine
|
||||
|
||||
class StartCallViewModel: ObservableObject {
|
||||
|
||||
static let TAG = "[StartCallViewModel]"
|
||||
|
||||
private var coreContext = CoreContext.shared
|
||||
|
||||
@Published var searchField: String = ""
|
||||
|
||||
var domain: String = ""
|
||||
|
||||
@Published var messageText: String = ""
|
||||
|
||||
@Published var participants: [SelectedAddressModel] = []
|
||||
|
||||
@Published var operationInProgress: Bool = false
|
||||
|
||||
private var conferenceSuscriptions = Set<AnyCancellable?>()
|
||||
|
||||
init() {
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
coreContext.doOnCoreQueue { core in
|
||||
self.domain = core.defaultAccount?.params?.domain ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
func addParticipants(participantsToAdd: [SelectedAddressModel]) {
|
||||
var list = participants
|
||||
for selectedAddr in participantsToAdd {
|
||||
if let found = list.first(where: { $0.address.weakEqual(address2: selectedAddr.address) }) {
|
||||
Log.info("\(ScheduleMeetingViewModel.TAG) Participant \(found.address.asStringUriOnly()) already in list, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
list.append(selectedAddr)
|
||||
Log.info("\(ScheduleMeetingViewModel.TAG) Added participant \(selectedAddr.address.asStringUriOnly())")
|
||||
}
|
||||
Log.info("\(ScheduleMeetingViewModel.TAG) [\(list.count - participants.count) participants added, now there are \(list.count) participants in list")
|
||||
|
||||
participants = list
|
||||
}
|
||||
|
||||
func createGroupCall() {
|
||||
coreContext.doOnCoreQueue { core in
|
||||
let account = core.defaultAccount
|
||||
if account == nil {
|
||||
Log.error(
|
||||
"\(StartCallViewModel.TAG) No default account found, can't create group call!"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = true
|
||||
}
|
||||
|
||||
do {
|
||||
let conferenceInfo = try Factory.Instance.createConferenceInfo()
|
||||
conferenceInfo.organizer = account!.params?.identityAddress
|
||||
conferenceInfo.subject = self.messageText
|
||||
|
||||
var participantsList: [ParticipantInfo] = []
|
||||
self.participants.forEach { participant in
|
||||
do {
|
||||
let info = try Factory.Instance.createParticipantInfo(address: participant.address)
|
||||
// For meetings, all participants must have Speaker role
|
||||
info.role = Participant.Role.Speaker
|
||||
participantsList.append(info)
|
||||
} catch let error {
|
||||
Log.error(
|
||||
"\(StartCallViewModel.TAG) Can't create ParticipantInfo: \(error)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
self.participants.removeAll()
|
||||
|
||||
conferenceInfo.addParticipantInfos(participantInfos: participantsList)
|
||||
|
||||
Log.info(
|
||||
"\(StartCallViewModel.TAG) Creating group call with subject \(self.messageText) and \(participantsList.count) participant(s)"
|
||||
)
|
||||
|
||||
let conferenceScheduler = try core.createConferenceScheduler()
|
||||
self.conferenceAddDelegate(core: core, conferenceScheduler: conferenceScheduler)
|
||||
conferenceScheduler.account = account
|
||||
// Will trigger the conference creation/update automatically
|
||||
conferenceScheduler.info = conferenceInfo
|
||||
} catch let error {
|
||||
Log.error(
|
||||
"\(StartCallViewModel.TAG) createGroupCall: \(error)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func conferenceAddDelegate(core: Core, conferenceScheduler: ConferenceScheduler) {
|
||||
self.conferenceSuscriptions.insert(conferenceScheduler.publisher?.onStateChanged?.postOnCoreQueue {
|
||||
(conferenceScheduler: ConferenceScheduler, state: ConferenceScheduler.State) in
|
||||
Log.info("\(StartCallViewModel.TAG) Conference scheduler state is \(state)")
|
||||
if state == ConferenceScheduler.State.Ready {
|
||||
self.conferenceSuscriptions.removeAll()
|
||||
|
||||
let conferenceAddress = conferenceScheduler.info?.uri
|
||||
if conferenceAddress != nil {
|
||||
Log.info(
|
||||
"\(StartCallViewModel.TAG) Conference info created, address is \(conferenceAddress?.asStringUriOnly() ?? "Error conference address")"
|
||||
)
|
||||
|
||||
self.startVideoCall(core: core, conferenceAddress: conferenceAddress!)
|
||||
} else {
|
||||
Log.error("\(StartCallViewModel.TAG) Conference info URI is null!")
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_group_call_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
}
|
||||
} else if state == ConferenceScheduler.State.Error {
|
||||
self.conferenceSuscriptions.removeAll()
|
||||
Log.error("\(StartCallViewModel.TAG) Failed to create group call!")
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_group_call_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func startVideoCall(core: Core, conferenceAddress: Address) {
|
||||
do {
|
||||
TelecomManager.shared.doCallWithCore(addr: conferenceAddress, isVideo: true, isConference: true)
|
||||
} catch let error {
|
||||
Log.error(
|
||||
"\(StartCallViewModel.TAG) StartVideoCall: \(error)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ class AddParticipantsViewModel: ObservableObject {
|
|||
} else {
|
||||
Log.info("[\(AddParticipantsViewModel.TAG)] Adding participant \(addr.asStringUriOnly()) to selection")
|
||||
ContactAvatarModel.getAvatarModelFromAddress(address: addr) { avatarResult in
|
||||
self.participantsToAdd.append(SelectedAddressModel(addr: addr, avModel: avatarResult))
|
||||
DispatchQueue.main.async {
|
||||
self.participantsToAdd.append(SelectedAddressModel(addr: addr, avModel: avatarResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,4 +35,8 @@ extension UIApplication {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func endEditing() {
|
||||
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue