Compare commits

...

46 commits

Author SHA1 Message Date
Benoit Martins
df3904d76c Updated CHANGELOG & bumped version code to 6.0.2 2025-09-26 16:22:55 +02:00
Benoit Martins
a40c339ce9 Fix EditContactFragment view and allow '+' in number dialer 2025-09-26 15:57:07 +02:00
Benoit Martins
955432c17f Fix dial plan selector and dial plan default 2025-09-26 14:07:57 +02:00
Benoit Martins
d993add9a7 Add advanced settings to third-party SIP account login view 2025-09-25 16:18:49 +02:00
Benoit Martins
dab585ccd5 Update translations from Weblate 2025-09-23 17:12:13 +02:00
Benoit Martins
3451a11970 Fix threading issues in saveImage and saveFriend in ContactsManager 2025-09-22 15:17:03 +02:00
Benoit Martins
28f9555aab New Fix crash when editing a contact by safely unwrapping friend/photo 2025-09-22 11:54:33 +02:00
Benoit Martins
38c50ec59c Revert "Fix crash when editing a contact by safely unwrapping friend/photo"
This reverts commit b822e7c552.
2025-09-22 10:24:07 +02:00
Benoit Martins
b822e7c552 Fix crash when editing a contact by safely unwrapping friend/photo 2025-09-19 16:48:59 +02:00
Benoit Martins
695c7ef300 Change French translation of manage_account_settings 2025-09-19 16:24:46 +02:00
Benoit Martins
838f95f6e3 Disable meetings view when audio/video conference factory address is missing 2025-09-19 16:08:48 +02:00
Benoit Martins
084c2821e2 Stop requesting device list in AccountModel initializer 2025-09-19 11:36:11 +02:00
Benoit Martins
e67c6b04a5 Disable push notifications when pushNotificationAllowed is false 2025-09-19 11:15:18 +02:00
Benoit Martins
a4ddc70e1a Update config files 2025-09-19 11:14:22 +02:00
Benoit Martins
4ab97cdb33 Fix meeting scheduler 2025-09-18 21:22:21 +02:00
Benoit Martins
7b32b149f8 Update friend list subscriptions on Core queue 2025-09-15 15:52:34 +02:00
Benoit Martins
72a1dc8431 Compute notifications count in core queue 2025-09-15 15:33:58 +02:00
Benoit Martins
c8fbba99b0 Fix call video display (nativePreviewWindow and nativeVideoWindow) 2025-09-15 15:17:56 +02:00
Benoit Martins
f53348236e Fix call video display 2025-09-15 14:25:27 +02:00
Benoit Martins
9a98190e86 Fix issue with meeting scheduling 2025-09-15 13:36:47 +02:00
Benoit Martins
974f1ca0fe Reset the displayed chat room also when the chat room is empty 2025-09-15 12:07:41 +02:00
Benoit Martins
93294b2a91 Fix crash on core.queue by safely reading Strings from config 2025-09-15 11:35:42 +02:00
Benoit Martins
63c5fcb5fd Ensure call termination is executed on the Core queue 2025-09-15 11:16:15 +02:00
Benoit Martins
f074caa49a Add a burger button to open the side menu 2025-09-15 11:08:56 +02:00
Benoit Martins
924929a1ad Change the layout icon in the conference call 2025-09-15 10:50:27 +02:00
Benoit Martins
9240dc352f Updated CHANGELOG & bumped version code to 6.0.1 2025-09-12 14:04:46 +02:00
Benoit Martins
482079d873 Update translations from Weblate 2025-09-12 12:03:35 +00:00
Benoit Martins
79ac4f0434 Add Done button toolbar to number pads 2025-09-11 18:14:54 +02:00
Benoit Martins
095d3e3551 Fix avatar photo refresh 2025-09-11 18:00:21 +02:00
Benoit Martins
e2644a2347 Fix onEphemeralMessageTimerStarted callback 2025-09-11 17:51:18 +02:00
Benoit Martins
b0ee9e80d4 Fix crash in updateEncryption by safely handling optional currentCall 2025-09-11 16:53:23 +02:00
Benoit Martins
a211d0c994 Fix sorted list in MagicSearch when friend is nil 2025-09-11 16:32:00 +02:00
Benoit Martins
7e2c429052 Fix friend list refresh triggered by onPresenceReceived 2025-09-11 16:32:00 +02:00
Benoit Martins
165b718514 Fix crash when adding or removing SIP addresses and phone numbers in EditContactFragment 2025-09-05 16:51:49 +02:00
Benoit Martins
af981603ab Use saveImage on core queue 2025-09-05 15:49:51 +02:00
Benoit Martins
c5cef9119a Update textToImage to generate image on the core queue 2025-09-05 15:25:42 +02:00
Benoit Martins
68ed0905b9 Fix awaitDataWrite execution on main queue 2025-09-05 15:20:10 +02:00
Benoit Martins
bdc104c22f Send DTMF on the core queue 2025-09-05 14:39:45 +02:00
Benoit Martins
0c8c2d7ecb Prevent crash by copying Friend addresses and phone numbers before removal 2025-09-05 14:20:04 +02:00
Benoit Martins
09e1ca1df5 Ensure core is On before stopping it on background entry 2025-09-04 16:41:00 +02:00
Benoit Martins
bd4f59085d Use point_to_point string for encrypted calls in conference 2025-09-04 15:16:35 +02:00
Benoit Martins
25d960c8a8 Add help view to login page 2025-09-04 15:02:34 +02:00
Benoit Martins
f79506590f Hide VFS setting 2025-09-04 15:02:24 +02:00
Benoit Martins
295248455b Fix textToImage crash 2025-09-04 12:01:17 +02:00
Benoit Martins
a327d96900 Update CHANGELOG.md 2025-09-02 11:54:26 +02:00
Benoit Martins
5ca63b577a Update README.md 2025-09-02 10:33:05 +02:00
44 changed files with 1511 additions and 712 deletions

View file

@ -10,51 +10,82 @@ Group changes to describe their impact on the project, as follows:
Fixed for any bug fixes.
Security to invite users to upgrade in case of vulnerabilities.
## [6.0.0] - 2025-03-11
## [6.0.2] - 2025-09-26
### Added
- Advanced settings to third-party SIP account login view
- Burger button to open the side menu
### Changed
- Layout icon in conference call
- Translations from Weblate
- Disable meetings view when audio/video conference factory address is missing
### Fixed
- EditContactFragment view and allow '+' in number dialer
- Dial plan selector and dial plan default
- Crash when editing a contact by safely unwrapping friend/photo
- Meeting scheduler
## [6.0.1] - 2025-09-12
### Added
- Done button toolbar to number pads
- Help view to login page
### Changed
- textToImage updated to generate image on the core queue
- Send DTMF execution moved to the core queue
- Use saveImage on core queue
- Use point_to_point string for encrypted calls in conference
- Hide VFS setting
### Fixed
- Avatar photo refresh
- onEphemeralMessageTimerStarted callback
- Crash in updateEncryption by safely handling optional currentCall
- Sorted list in MagicSearch when friend is nil
- Friend list refresh triggered by onPresenceReceived
- Crash when adding or removing SIP addresses and phone numbers in EditContactFragment
- awaitDataWrite execution on main queue
- Crash by copying Friend addresses and phone numbers before removal
- Ensure core is On before stopping it on background entry
- textToImage crash
## [6.0.0] - 2025-09-01
6.0.0 release is a complete rework of Linphone, with a fully redesigned UI, so it is impossible to list everything here.
### Changed
- Separated threads: Contrary to previous versions, our SDK is now running in it's own thread, meaning it won't freeze the UI anymore in case of heavy work, thus reducing the number of ANR and greatly increasing the fluidity of the app.
- Separated threads: Contrary to previous versions, our SDK is now running in it's own thread, meaning it won't freeze the UI anymore in case of heavy work.
- Asymmetrical video : you no longer need to send your own camera feed to receive the one from the remote end of the call, and vice versa.
- Improved multi account: you'll only see history, conversations, meetings etc... related to currently selected account, and you can switch the default account in two clicks.
- Call transfer: Blind & Attended call transfer have been merged into one: during a call, if you initiate a transfer action, either pick another call to do the attended transfer or select a contact from the list (you can input a SIP URI not already in the suggestions list) to start a blind transfer.
- User can only send up to 12 files in a single chat message.
- IMDNs are now only sent to the message sender, preventing huge traffic in large groups, and thus the delivery status icon for received messages is now hidden in groups (as it was in 1-1 conversations).
- Settings: a lot of them are gone, the one that are still there have been reworked to increase user friendliness.
- Default screen (between contacts, call history, conversations & meetings list) will change depending on where you were when the app was paused or killed, and you will return to that last visited screen on the next startup.
- Gradle files have been migrated from Groovy to Kotlin DSL, and dependencies are now in a separated file (libs.versions.toml).
- Account creation no longer allows you to use your phone number as username, but it is still required to provide it to receive activation code by SMS.
- Minimum supported iOS version is now 15.
- Some settings have changed name and/or section in linphonerc file.
### Added
- Contacts trust: contacts for which all devices have been validated through a ZRTP call with SAS exchange are now highlighted with a blue circle (and with a red one in case of mistrust). That trust is now handled at contact level (instead of conversation level in previous versions).
- Media & documents exchanged in a conversation can be easily found through a dedicated screen.
- A brand new chat message search feature has been added to conversations.
- You can now react to a chat message using any emoji.
- If next message is also a voice recording, playback will automatically start after the currently playing one ends.
- Chat while in call: a shortcut to a conversation screen with the remote.
- Chat while in a conference: if the conference has a text stream enabled, you can chat with the other participants of the conference while it lasts. At the end, you'll find the messages history in the call history (and not in the list of conversations).
- Auto export of media to native gallery even when auto download is enabled (but still not if VFS is enabled nor for ephemeral messages).
- Save / export document & media from ephemeral messages will be disabled, and secure policy that prevents screenshots will be enforced in file viewer even if the setting is disabled.
- Notification showing upload/download of files shared through chat will let user know the progress and keep the app alive during that process.
- Screen sharing in conference: only desktop app starting with 6.0 version is able to start it, but on mobiles you'll be able to see it.
- Security focus: security & trust is more visible than ever, and unsecure conversations & calls are even more visible than before.
- OpenID: when used with a SSO compliant SIP server (such as Flexisip), we support single-sign-on login.
- MWI support: display and allow to call your voicemail when you have new messages (if supported by your VoIP provider and properly configured in your account params).
- CCMP support: if you configure a CCMP server URL in your accounts params, it will be used when scheduling meetings & to fetch list of meetings you've organized/been invited to.
- Devices list: check on which device your sip.linphone.org account is connected and the last connection date & time (like on subscribe.linphone.org).
- Protobuf dependency to allow logging native crashes stack traces at next app startup.
- Dialer & in-call numpad show letters under the digit.
### Removed
- Dialer: the previous home screen (dialer) has been removed, you'll find it as an input option in the new start call screen.
- Peer-to-peer: a SIP account (sip.linphone.org or other) is now required.
- Contacts: we no longer add contacts created in-app in the native addressbook (WRITE_CONTACTS permission was removed), but we still import them if you grant us the READ_CONTACTS permission.
### Fixed
- AAudio driver no longer causes delay when switching between devices (SDK fix).
## [5.2.0] - 2023-28-12
### Added

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "layout.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,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,16V96H40V56ZM40,112H96v88H40Zm176,88H112V112H216v88Z"></path></svg>

After

Width:  |  Height:  |  Size: 274 B

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "list.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,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"></path></svg>

After

Width:  |  Height:  |  Size: 277 B

View file

@ -154,25 +154,22 @@ final class ContactsManager: ObservableObject {
let imageThumbnail = UIImage(data: contact.thumbnailImageData ?? Data())
if let image = imageThumbnail {
DispatchQueue.main.async {
self.saveImage(
image: image,
name: contact.givenName + contact.familyName,
prefix: "",
contact: newContact, linphoneFriend: self.nativeAddressBookFriendList, existingFriend: nil) {
dispatchGroup.leave()
}
}
self.saveImage(
image: image,
name: contact.givenName + contact.familyName,
prefix: "",
contact: newContact, linphoneFriend: self.nativeAddressBookFriendList, existingFriend: nil) {
dispatchGroup.leave()
}
} else {
self.textToImageInMainThread(firstName: contact.givenName, lastName: contact.familyName) { image in
self.saveImage(
image: image,
name: contact.givenName + contact.familyName,
prefix: "-default",
contact: newContact, linphoneFriend: self.nativeAddressBookFriendList, existingFriend: nil) {
dispatchGroup.leave()
}
}
let image = self.textToImage(firstName: contact.givenName, lastName: contact.familyName)
self.saveImage(
image: image,
name: contact.givenName + contact.familyName,
prefix: "-default",
contact: newContact, linphoneFriend: self.nativeAddressBookFriendList, existingFriend: nil) {
dispatchGroup.leave()
}
}
})
@ -197,154 +194,133 @@ final class ContactsManager: ObservableObject {
}
}
func textToImageInMainThread(firstName: String, lastName: String, completion: @escaping (UIImage) -> Void) {
DispatchQueue.main.async {
let lblNameInitialize = UILabel()
lblNameInitialize.frame.size = CGSize(width: 200.0, height: 200.0)
lblNameInitialize.font = UIFont(name: "NotoSans-ExtraBold", size: 80)
lblNameInitialize.textColor = UIColor(Color.grayMain2c600)
func textToImage(firstName: String?, lastName: String?) -> UIImage {
let firstInitial = firstName?.first.map { String($0) } ?? ""
let lastInitial = lastName?.first.map { String($0) } ?? ""
let textToDisplay = (firstInitial + lastInitial).uppercased()
let size = CGSize(width: 200, height: 200)
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { _ in
let rect = CGRect(origin: .zero, size: size)
let textToDisplay = (firstName.first.map { String($0) } ?? "") + (lastName.first.map { String($0) } ?? "")
UIColor(Color.grayMain2c200).setFill()
UIBezierPath(roundedRect: rect, cornerRadius: 10).fill()
lblNameInitialize.text = textToDisplay.uppercased()
lblNameInitialize.textAlignment = .center
lblNameInitialize.backgroundColor = UIColor(Color.grayMain2c200)
lblNameInitialize.layer.cornerRadius = 10.0
lblNameInitialize.clipsToBounds = true
UIGraphicsBeginImageContext(lblNameInitialize.frame.size)
defer { UIGraphicsEndImageContext() }
guard let context = UIGraphicsGetCurrentContext() else {
completion(UIImage())
return
}
lblNameInitialize.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
completion(image)
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont(name: "NotoSans-ExtraBold", size: 80) ?? UIFont.boldSystemFont(ofSize: 80),
.foregroundColor: UIColor(Color.grayMain2c600),
.paragraphStyle: paragraph
]
let textSize = textToDisplay.size(withAttributes: attributes)
let textRect = CGRect(
x: (size.width - textSize.width) / 2,
y: (size.height - textSize.height) / 2,
width: textSize.width,
height: textSize.height
)
textToDisplay.draw(in: textRect, withAttributes: attributes)
}
}
func textToImage(firstName: String, lastName: String) -> UIImage {
let lblNameInitialize = UILabel()
lblNameInitialize.frame.size = CGSize(width: 200.0, height: 200.0)
lblNameInitialize.font = UIFont(name: "NotoSans-ExtraBold", size: 80)
lblNameInitialize.textColor = UIColor(Color.grayMain2c600)
var textToDisplay = ""
if firstName.first != nil {
textToDisplay += String(firstName.first!)
}
if lastName.first != nil {
textToDisplay += String(lastName.first!)
}
lblNameInitialize.text = textToDisplay.uppercased()
lblNameInitialize.textAlignment = .center
lblNameInitialize.backgroundColor = UIColor(Color.grayMain2c200)
lblNameInitialize.layer.cornerRadius = 10.0
var IBImgViewUserProfile = UIImage()
UIGraphicsBeginImageContext(lblNameInitialize.frame.size)
lblNameInitialize.layer.render(in: UIGraphicsGetCurrentContext()!)
IBImgViewUserProfile = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return IBImgViewUserProfile
}
func saveImage(image: UIImage, name: String, prefix: String, contact: Contact, linphoneFriend: String, existingFriend: Friend?, completion: @escaping () -> Void) {
guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else {
return
}
awaitDataWrite(data: data, name: name, prefix: prefix) { _, result in
awaitDataWrite(data: data, name: name, prefix: prefix) { result in
self.saveFriend(result: result, contact: contact, existingFriend: existingFriend) { resultFriend in
if resultFriend != nil {
if linphoneFriend != self.nativeAddressBookFriendList && existingFriend == nil {
if let linphoneFL = self.linphoneFriendList, linphoneFriend == linphoneFL.displayName {
_ = linphoneFL.addFriend(linphoneFriend: resultFriend!)
} else if let linphoneFL = self.tempRemoteFriendList {
_ = linphoneFL.addFriend(linphoneFriend: resultFriend!)
}
} else if existingFriend == nil {
if let friendListTmp = self.friendList {
_ = friendListTmp.addLocalFriend(linphoneFriend: resultFriend!)
self.coreContext.doOnCoreQueue { core in
if let friend = resultFriend {
if linphoneFriend != self.nativeAddressBookFriendList && existingFriend == nil {
if let linphoneFL = self.linphoneFriendList, linphoneFriend == linphoneFL.displayName {
_ = linphoneFL.addFriend(linphoneFriend: friend)
} else if let linphoneFL = self.tempRemoteFriendList {
_ = linphoneFL.addFriend(linphoneFriend: friend)
}
} else if existingFriend == nil {
if let friendListTmp = self.friendList {
_ = friendListTmp.addLocalFriend(linphoneFriend: friend)
}
}
}
DispatchQueue.main.async {
completion()
}
}
completion()
}
}
}
func saveFriend(result: String, contact: Contact, existingFriend: Friend?, completion: @escaping (Friend?) -> Void) {
self.coreContext.doOnCoreQueue { core in
do {
// Create or use existing friend
let friend = try existingFriend ?? core.createFriend()
// Strong capture in closure to avoid threading issues
friend.edit()
friend.nativeUri = contact.identifier
try friend.setName(newValue: contact.firstName + " " + contact.lastName)
let friendvCard = friend.vcard
if friendvCard != nil {
friendvCard!.givenName = contact.firstName
friendvCard!.familyName = contact.lastName
// Safely update vCard
if let vcard = friend.vcard {
vcard.givenName = contact.firstName
vcard.familyName = contact.lastName
}
friend.organization = contact.organizationName
var friendAddresses: [Address] = []
friend.addresses.forEach({ address in
friend.removeAddress(address: address)
})
contact.sipAddresses.forEach { sipAddress in
if !sipAddress.isEmpty {
let address = core.interpretUrl(url: sipAddress, applyInternationalPrefix: true)
if address != nil && ((friendAddresses.firstIndex(where: {$0.asString() == address?.asString()})) == nil) {
friend.addAddress(address: address!)
friendAddresses.append(address!)
}
}
}
var friendPhoneNumbers: [PhoneNumber] = []
friend.phoneNumbersWithLabel.forEach({ phoneNumber in
friend.removePhoneNumberWithLabel(phoneNumber: phoneNumber)
})
contact.phoneNumbers.forEach { phone in
do {
if (friendPhoneNumbers.firstIndex(where: {$0.num == phone.num})) == nil {
let labelDrop = String(phone.numLabel.dropFirst(4).dropLast(4))
let phoneNumber = try Factory.Instance.createFriendPhoneNumber(phoneNumber: phone.num, label: labelDrop)
friend.addPhoneNumberWithLabel(phoneNumber: phoneNumber)
friendPhoneNumbers.append(phone)
}
} catch let error {
print("\(#function) - Failed to create friend phone number for \(phone.numLabel):", error)
}
}
friend.photo = "file:/" + result
friend.organization = contact.organizationName
friend.jobTitle = contact.jobTitle
// Clear existing addresses and add new ones
friend.addresses.forEach { friend.removeAddress(address: $0) }
for sipAddress in contact.sipAddresses where !sipAddress.isEmpty {
if let address = core.interpretUrl(url: sipAddress, applyInternationalPrefix: true),
!friend.addresses.contains(where: { $0.asString() == address.asString() }) {
friend.addAddress(address: address)
}
}
// Clear existing phone numbers and add new ones
friend.phoneNumbersWithLabel.forEach { friend.removePhoneNumberWithLabel(phoneNumber: $0) }
for phone in contact.phoneNumbers {
do {
let labelDrop = String(phone.numLabel.dropFirst(4).dropLast(4))
let phoneNumber = try Factory.Instance.createFriendPhoneNumber(phoneNumber: phone.num, label: labelDrop)
friend.addPhoneNumberWithLabel(phoneNumber: phoneNumber)
} catch {
print("saveFriend - Failed to create friend phone number for \(phone.numLabel):", error)
}
}
// Set photo
friend.photo = "file:/" + result
// Linphone subscription settings
try friend.setSubscribesenabled(newValue: false)
try friend.setIncsubscribepolicy(newValue: .SPDeny)
// Commit changes
friend.done()
// Notify completion safely
completion(friend)
} catch let error {
print("Failed to enumerate contact", error)
} catch {
print("saveFriend - Failed to save friend:", error)
completion(nil)
}
}
}
func getImagePath(friendPhotoPath: String) -> URL {
let friendPath = String(friendPhotoPath.dropFirst(6))
@ -354,26 +330,21 @@ final class ContactsManager: ObservableObject {
return imagePath
}
func awaitDataWrite(data: Data, name: String, prefix: String, completion: @escaping ((), String) -> Void) {
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
func awaitDataWrite(data: Data, name: String, prefix: String, completion: @escaping (String) -> Void) {
guard let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion("")
return
}
if directory != nil {
DispatchQueue.main.async {
do {
if let urlName = URL(string: name + prefix) {
let imagePath = urlName.absoluteString.replacingOccurrences(of: "%", with: "")
let decodedData: () = try data.write(to: directory!.appendingPathComponent(imagePath + ".png"))
completion(decodedData, imagePath + ".png")
} else {
completion((), "")
}
} catch {
print("Error: ", error)
completion((), "")
}
}
do {
let fileName = name + prefix + ".png"
let fileURL = directory.appendingPathComponent(fileName)
try data.write(to: fileURL)
completion(fileName)
} catch {
print("Error writing image: \(error)")
completion("")
}
}
@ -440,8 +411,11 @@ final class ContactsManager: ObservableObject {
if status == .Successful {
if friendList.displayName != self.nativeAddressBookFriendList && friendList.displayName != self.linphoneAddressBookFriendList {
if let tempRemoteFriendList = self.tempRemoteFriendList {
tempRemoteFriendList.friends.forEach { friend in
_ = tempRemoteFriendList.removeFriend(linphoneFriend: friend)
tempRemoteFriendList.friends.forEach { friend in
if let friendAddress = friend.address,
friendList.friends.contains(where: { $0.address?.weakEqual(address2: friendAddress) == true }) {
_ = tempRemoteFriendList.removeFriend(linphoneFriend: friend)
}
}
}
}
@ -464,27 +438,82 @@ final class ContactsManager: ObservableObject {
imageData: ""
)
self.textToImageInMainThread(firstName: friend.name ?? addressTmp, lastName: "") { image in
self.saveImage(
image: image,
name: friend.name ?? addressTmp,
prefix: "-default",
contact: newContact, linphoneFriend: friendList.displayName ?? "No Display Name", existingFriend: nil) {
dispatchGroup.leave()
}
}
let image = self.textToImage(firstName: friend.name ?? addressTmp, lastName: "")
self.saveImage(
image: image,
name: friend.name ?? addressTmp,
prefix: "-default",
contact: newContact, linphoneFriend: friendList.displayName ?? "No Display Name", existingFriend: nil) {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
MagicSearchSingleton.shared.searchForContacts()
if let linphoneFL = self.tempRemoteFriendList {
linphoneFL.updateSubscriptions()
}
self.coreContext.doOnCoreQueue { _ in
MagicSearchSingleton.shared.searchForContacts()
if let linphoneFL = self.tempRemoteFriendList {
linphoneFL.updateSubscriptions()
}
}
}
}
},
onPresenceReceived: { (friendList: FriendList, friends: [Friend?]) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onPresenceReceived \(friends.count)")
if (friendList.isSubscriptionBodyless) {
Log.info("\(ContactsManager.TAG) Bodyless friendlist \(friendList.displayName ?? "No Display Name") presence received")
if friendList.displayName != self.nativeAddressBookFriendList && friendList.displayName != self.linphoneAddressBookFriendList {
if let tempRemoteFriendList = self.tempRemoteFriendList {
tempRemoteFriendList.friends.forEach { friend in
if let friendAddress = friend.address,
friends.contains(where: { $0?.address?.weakEqual(address2: friendAddress) == true }) {
_ = tempRemoteFriendList.removeFriend(linphoneFriend: friend)
}
}
}
}
let dispatchGroup = DispatchGroup()
friends.forEach { friend in
dispatchGroup.enter()
if let friend = friend {
let addressTmp = friend.address?.clone()?.asStringUriOnly() ?? ""
Log.debug("\(ContactsManager.TAG) Newly discovered SIP Address \(addressTmp) for friend \(friend.name ?? "No Name") in bodyless list \(friendList.displayName ?? "No Display Name")")
let newContact = Contact(
identifier: UUID().uuidString,
firstName: friend.name ?? addressTmp,
lastName: "",
organizationName: "",
jobTitle: "",
displayName: friend.address?.displayName ?? "",
sipAddresses: friend.addresses.map { $0.asStringUriOnly() },
phoneNumbers: [],
imageData: ""
)
let image = self.textToImage(firstName: friend.name ?? addressTmp, lastName: "")
self.saveImage(
image: image,
name: friend.name ?? addressTmp,
prefix: "-default",
contact: newContact, linphoneFriend: friendList.displayName ?? "No Display Name", existingFriend: nil) {
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
self.coreContext.doOnCoreQueue { _ in
MagicSearchSingleton.shared.searchForContacts()
if let linphoneFL = self.tempRemoteFriendList {
linphoneFL.updateSubscriptions()
}
}
}
}
},
onNewSipAddressDiscovered: { (friendList: FriendList, linphoneFriend: Friend, sipUri: String) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onNewSipAddressDiscovered \(linphoneFriend.name ?? "")")
@ -522,6 +551,8 @@ final class ContactsManager: ObservableObject {
}
)
self.friendListDelegate = friendListDelegateTmp
CoreContext.shared.mCore.friendsLists.forEach { friendList in
friendList.addDelegate(delegate: friendListDelegateTmp)
}
@ -558,15 +589,17 @@ final class ContactsManager: ObservableObject {
for contact in avatarListModel {
contact.$starred
.sink { [weak self] _ in
self?.starredChangeTrigger = UUID() // 🔁 Déclenche le refresh de la vue
self?.starredChangeTrigger = UUID()
}
.store(in: &cancellables)
}
}
func updateSubscriptionsLinphoneList() {
if let linphoneFL = self.linphoneFriendList {
linphoneFL.updateSubscriptions()
self.coreContext.doOnCoreQueue { _ in
if let linphoneFL = self.linphoneFriendList {
linphoneFL.updateSubscriptions()
}
}
}
}

View file

@ -146,7 +146,8 @@ class CoreContext: ObservableObject {
self.mCore = try? Factory.Instance.createSharedCoreWithConfig(config: Config.get(), systemContext: Unmanaged.passUnretained(coreQueue).toOpaque(), appGroupId: Config.appGroupName, mainCore: true)
self.mCore.callkitEnabled = true
self.mCore.pushNotificationEnabled = true
self.mCore.pushNotificationEnabled = self.mCore.defaultAccount?.params?.pushNotificationAllowed ?? false
let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
@ -215,12 +216,6 @@ class CoreContext: ObservableObject {
newParams?.pushNotificationConfig?.provider = "apns" + pushEnvironment
}
if account.params?.internationalPrefix == nil {
Log.info("Account \(account.displayName()): no international prefix set, adding 33 FRA by default: \(account.params?.internationalPrefix ?? "NIL")")
newParams?.internationalPrefix = "33"
newParams?.internationalPrefixIsoCountryCode = "FRA"
newParams?.useInternationalPrefixForCallsAndChats = true
}
account.params = newParams
}
@ -424,26 +419,24 @@ class CoreContext: ObservableObject {
func onEnterForeground() {
coreQueue.async {
// We can't rely on defaultAccount?.params?.isPublishEnabled
// as it will be modified by the SDK when changing the presence status
Log.info("[onEnterForegroundOrBackground] Entering foreground")
try? self.mCore.start()
}
}
func onEnterBackground() {
coreQueue.async {
// We can't rely on defaultAccount?.params?.isPublishEnabled
// as it will be modified by the SDK when changing the presence status
Log.info("App is in background, un-PUBLISHING presence info")
Log.info("[onEnterForegroundOrBackground] Entering background, un-PUBLISHING presence info")
// We don't use ConsolidatedPresence.Busy but Offline to do an unsubscribe,
// Flexisip will handle the Busy status depending on other devices
self.updatePresence(core: self.mCore, presence: ConsolidatedPresence.Offline)
self.updatePresence(core: self.mCore, presence: .Offline)
self.mCore.iterate()
if self.mCore.currentCall == nil {
if self.mCore.currentCall == nil && self.mCore.globalState == .On {
Log.info("[onEnterForegroundOrBackground] Stopping core because no active calls")
self.mCore.stop()
} else {
Log.info("[onEnterForegroundOrBackground] Skipped stop: core not fully On or active call in progress")
}
}
}

View file

@ -50,7 +50,8 @@ class CorePreferences {
static var checkForUpdateServerUrl: String {
get {
return Config.get().getString(section: "misc", key: "version_check_url_root", defaultString: "")
let raw = Config.get().getString(section: "misc", key: "version_check_url_root", defaultString: "")
return safeString(raw, defaultValue: "")
}
set {
Config.get().setString(section: "misc", key: "version_check_url_root", value: newValue)
@ -86,7 +87,8 @@ class CorePreferences {
static var deviceName: String {
get {
return Config.get().getString(section: "app", key: "device", defaultString: "").trimmingCharacters(in: .whitespaces)
let raw = Config.get().getString(section: "app", key: "device", defaultString: "").trimmingCharacters(in: .whitespaces)
return safeString(raw, defaultValue: "")
}
set {
Config.get().setString(section: "app", key: "device", value: newValue.trimmingCharacters(in: .whitespaces))
@ -131,7 +133,8 @@ class CorePreferences {
static var contactsFilter: String {
get {
return Config.get().getString(section: "ui", key: "contacts_filter", defaultString: "")
let raw = Config.get().getString(section: "ui", key: "contacts_filter", defaultString: "")
return safeString(raw, defaultValue: "")
}
set {
Config.get().setString(section: "ui", key: "contacts_filter", value: newValue)
@ -177,7 +180,8 @@ class CorePreferences {
static var themeMainColor: String {
get {
return Config.get().getString(section: "ui", key: "theme_main_color", defaultString: "orange")
let raw = Config.get().getString(section: "ui", key: "theme_main_color", defaultString: "orange")
return safeString(raw, defaultValue: "orange")
}
set {
Config.get().setString(section: "ui", key: "theme_main_color", value: newValue)
@ -244,7 +248,8 @@ class CorePreferences {
static var defaultDomain: String {
get {
return Config.get().getString(section: "app", key: "default_domain", defaultString: "sip.linphone.org")
let raw = Config.get().getString(section: "app", key: "default_domain", defaultString: "sip.linphone.org")
return safeString(raw, defaultValue: "sip.linphone.org")
}
set {
Config.get().setString(section: "app", key: "default_domain", value: newValue)
@ -283,4 +288,12 @@ class CorePreferences {
}
}
}
private static func safeString(_ raw: String?, defaultValue: String = "") -> String {
guard let raw = raw else { return defaultValue }
if let data = raw.data(using: .utf8), let s = String(data: data, encoding: .utf8) {
return s
}
return defaultValue
}
}

View file

@ -43,7 +43,7 @@
"assistant_account_login_forbidden_error" = "Falscher Benutzername oder Passwort";
"assistant_account_register" = "Registrieren";
"assistant_account_register_push_notification_not_received_error" = "Push Benachrichtigung mit Authentifizierungstoken nicht innerhalb von 5 Sekunden empfangen, bitte versuchen Sie es später erneut";
"assistant_account_register_unexpected_error" = "Unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.";
"assistant_account_register_unexpected_error" = "Unerwarteter Fehler ist aufgetreten, bitte versuchen Sie es später erneut";
"assistant_already_have_an_account" = "Haben Sie bereits ein Konto?";
"assistant_create_account_using_email_on_our_web_platform" = "Erstellen Sie mit Ihrer E-Mail ein Konto bei:";
"assistant_dialog_confirm_phone_number_title" = "Telefonnummer bestätigen";
@ -318,7 +318,7 @@
"settings_calls_auto_record_title" = "Automatische Anrufaufzeichnung starten";
"settings_calls_calibrate_echo_canceller_title" = "Echokompensator kalibrieren";
"settings_calls_change_ringtone_title" = "Klingelton ändern";
"settings_calls_echo_canceller_subtitle" = "Verhindert, dass das Echo am entfernten Ende gehört wird, wenn kein Hardware Echokompensator verfügbar ist.";
"settings_calls_echo_canceller_subtitle" = "Verhindert, dass das Echo am entfernten Ende gehört wird, wenn kein Hardware Echokompensator verfügbar ist";
"settings_calls_echo_canceller_title" = "Verwenden Sie die Software Echounterdrückung";
"settings_calls_enable_fec_title" = "Video FEC aktivieren";
"settings_calls_enable_video_title" = "Video aktivieren";
@ -363,3 +363,147 @@
"welcome_page_title" = "Willkommen";
"conversation_end_to_end_encrypted_event_subtitle" = "Nachrichten in dieser Gespr sind Ende-zu-Ende verschlüsselt. Nur Ihr Gesprächspartner kann sie entschlüsseln.";
"conversation_end_to_end_encrypted_event_title" = "Ende-zu-Ende verschlüsselte Gespräch";
"assistant_permissions_post_notifications_title" = "**Postbenachrichtigungen:** Um informiert zu werden, wenn Sie eine Nachricht oder einen Anruf erhalten.";
"assistant_permissions_access_camera_title" = "**Auf Kamera zugreifen:** Zum Aufnehmen von Videos während Videoanrufen und Konferenzen.";
"assistant_permissions_read_contacts_title" = "**Kontakte lesen:** Um Ihre Kontakte anzuzeigen und herauszufinden, wer %@ verwendet.";
"account_settings_dialog_invalid_password_message" = "Verbindung fehlgeschlagen, da die Authentifizierung für das Konto fehlt oder ungültig ist.\n%@.\n\nSie können Ihr Passwort erneut eingeben oder Ihre Kontokonfiguration in den Einstellungen überprüfen.";
"assistant_account_creation_sms_confirmation_explanation" = "Wir haben einen Bestätigungscode an Ihre Telefonnummer %@ gesendet. Bitte geben Sie unten den Bestätigungscode ein:";
"assistant_dialog_confirm_phone_number_message" = "Möchten Sie wirklich die Telefonnummer %@ verwenden?";
"assistant_dialog_general_terms_and_privacy_policy_message" = "Indem Sie fortfahren, akzeptieren Sie unsere %@ und %@.";
"assistant_forgotten_password" = "Passwort vergessen?";
"assistant_invalid_uri_toast" = "Ungültige URI";
"assistant_permissions_record_audio_title" = "**Audio aufnehmen:** Damit Ihr Gesprächspartner Sie hören kann und um Sprachnachrichten aufzunehmen.";
"assistant_permissions_subtitle" = "Um %@ in vollem Umfang nutzen zu können, müssen Sie uns die folgenden Berechtigungen erteilen:";
"history_list_empty_with_filter_history" = "Keine Einträge entsprechen Ihrer Suche";
"history_title" = "Anrufliste";
"IM_MSG" = "Sie haben eine Nachricht erhalten";
"Interoperable" = "Interoperabel";
"conversation_event_participant_added" = "%@ ist beigetreten";
"drawer_menu_account_connection_status_refreshing" = "Aktualisieren...";
"failed_meeting_ics_invitation_not_sent_toast" = "ICS-Besprechungseinladungen an Teilnehmer konnten nicht gesendet werden";
"You will change this mode later" = "Sie werden diesen Modus später ändern";
"welcome_carousel_skip" = "Überspringen";
"welcome_page_2_message" = "Ihre Kommunikation ist dank unserer **Ende-zu-Ende-Verschlüsselung** sicher.";
"welcome_page_subtitle" = "in %@";
"conversation_event_participant_removed" = "%@ hat verlassen";
"username_error" = "Benutzername-Fehler";
": %@" = ": %@";
"*" = "*";
"**%@**" = "**%@**";
"#" = "#";
"%@" = "%@";
"%lld" = "%lld";
"%lld %@" = "%1$lld %2$@";
"%lld%%" = "%lld%%";
"+" = "+";
"|" = "|";
"❤️" = "❤️";
"👍" = "👍";
"😂" = "😂";
"😢" = "😢";
"😮" = "😮";
"0" = "0";
"1" = "1";
"2" = "2";
"3" = "3";
"4" = "4";
"5" = "5";
"6" = "6";
"7" = "7";
"8" = "8";
"9" = "9";
"assistant_third_party_sip_account_create_linphone_account" = "Ich möchte lieber ein Konto erstellen";
"assistant_third_party_sip_account_warning_explanation" = "Für einige Funktionen, wie z. B. Gruppennachrichten, Videokonferenzen usw., ist ein %@-Konto erforderlich.\n\nDiese Funktionen sind ausgeblendet, wenn Sie sich mit einem SIP-Konto eines Drittanbieters registrieren.\n\nUm diese Funktion in einem kommerziellen Projekt zu aktivieren, kontaktieren Sie uns bitte.";
"call_action_attended_transfer" = "Begleitete Übertragung";
"call_audio_device_type_bluetooth" = "Bluetooth (%@)";
"call_can_be_trusted_toast" = "Authentifiziertes Gerät";
"call_dialog_zrtp_validate_trust_message" = "Zu Ihrer Sicherheit müssen wir Ihr Endgerät authentifizieren. Bitte tauschen Sie Ihre Codes aus:";
"call_dialog_zrtp_validate_trust_warning_message" = "Zu Ihrer Sicherheit müssen wir Ihr Endgerät authentifizieren. Bitte tauschen Sie Ihre Codes aus:";
"Ce mode vous permet dêtre interopérable avec dautres services SIP.\nVos communications seront chiffrées de point à point. " = "Dieser Modus ermöglicht die Interoperabilität mit anderen SIP-Diensten.\nIhre Kommunikation wird Punkt-zu-Punkt verschlüsselt. ";
"Chiffrement de bout en bout de tous vos échanges, grâce au mode default vos communications sont à labri des regards." = "Ende-zu-Ende-Verschlüsselung aller Ihrer Kommunikationen. Dank des Standardmodus sind Ihre Kommunikationen vor neugierigen Blicken geschützt.";
"conference_name_error" = "Konferenznamen Fehler";
"contact_details_numbers_and_addresses_title" = "Telefonnummern &amp; SIP-Adressen";
"contact_dialog_delete_title" = "%@ löschen?";
"call_transfer_current_call_title" = "Anruf weiterleiten";
"call_zrtp_sas_validation_skip" = "Überspringen";
"calls_count_label" = "%@ Anrufe";
"contact_video_call_action" = "Videoanruf";
"contacts_list_filter_popup_see_linphone_only" = "%@ Kontakte siehen";
"conversation_composing_label_multiple" = "%@ sind zusammengestellt…";
"conversation_composing_label_single" = "%@ ist zusammengestellt…";
"conversation_ephemeral_messages_duration_multiple_days" = "%d Tage";
"conversation_event_admin_set" = "%@ ist Administrator";
"conversation_event_admin_unset" = "$@ ist nicht mehr Administrator";
"conversation_event_device_added" = "neues Gerät für %@";
"conversation_event_device_removed" = "Gerät für %@ entfernt";
"conversation_event_ephemeral_messages_lifetime_changed" = "Kurzlebige Nachrichten Lebensdauer beträgt jetzt %@";
"conversation_event_subject_changed" = "neues Betreff: %@";
"conversations_files_waiting_to_be_shared_single" = "1 Datei wartet auf Freigabe";
"conversations_files_waiting_to_be_shared_multiple" = "%@ Dateien warten auf Freigabe";
"conversation_info_participants_list_title" = "Gruppe Teilnehmer (%d)";
"conversation_message_meeting_cancelled_label" = "Das Besprechung wurde abgesagt!";
"conversation_one_to_one_hidden_subject" = "Dummy-Betreff";
"conversation_reply_to_message_title" = "Antwort auf: ";
"debug_logs_copied_to_clipboard_toast" = "Debug-Protokolle in die Zwischenablage kopiert";
"Default" = "Standard";
"Default mode" = "Standardmodus";
"dialog_close" = "Schließen";
"DTLS" = "DTLS";
"GC_MSG" = "Sie wurden zu einem Chatroom hinzugefügt";
"help_dialog_update_available_message" = "Eine neue Version %@ ist verfügbar. Möchten Sie aktualisieren?";
"manage_account_dialog_international_prefix_help_message" = "Wählen Sie Ihr Land aus, um Linphone die Zuordnung Ihrer Kontakte zuzulassen.";
"meeting_call_remove_no_participants" = "Zur Zeit kein Teilnehmer…";
"meeting_call_remove_participant_confirmation_message" = "Sind Sie sicher, dass Sie %@ entfernen möchten?";
"meeting_call_remove_participant_confirmation_title" = "Einen Teilnehmer entfernen";
"meeting_exported_as_calendar_event" = "Besprechung zum iPhone-Kalender hinzugefügt";
"meeting_failed_to_edit_toast" = "Das Bearbeiten der Besprechung ist fehlgeschlagen";
"meeting_schedule_failed_no_subject_or_participant_toast" = "Zum Erstellen eines Meetings ist ein Betreff und mindestens ein Teilnehmer erforderlich";
"meeting_waiting_room_joining_subtitle" = "Sie werden in Kürze verbunden sein";
"meetings_list_empty" = "Zur Zeit keine Besprechung…";
"menu_block_address" = "Die Adresse blockieren";
"menu_block_number" = "Die Nummer blockieren";
"menu_copy_sip_address" = "SIP-Addresse kopieren";
"message_copied_to_clipboard_toast" = "Nachricht in die Zwischenablage kopiert";
"message_delivery_info_read_title" = "Gelesen";
"message_delivery_info_received_title" = "Erhalten";
"message_delivery_info_sent_title" = "Gesendet";
"message_meeting_invitation_cancelled_notification" = "📅 Die Besprechung wurde abgesagt";
"message_meeting_invitation_notification" = "📅 Sie sind zu einer Besprechung eingeladen";
"message_meeting_invitation_updated_notification" = "📅 Besprechung wurde aktualisiert";
"message_reactions_info_all_title" = "Reaktionen";
"network_reachable_again" = "Netzwerk ist nun wieder erreichbar";
"None" = "Kein";
"notification_chat_message_reaction_received" = "%@ hat mit %@ auf: %@ reagiert";
"notification_chat_message_received_title" = "Nachricht erhalten";
"Personnalize your profil mode" = "Ihren Profilmodus personalisieren";
"picker_categories" = "Kategorien";
"qr_code_validated" = "QR-Code validiert";
"selected_participants_count" = "%@ ausgewählte Teilnehmer";
"settings_calls_calibrate_echo_canceller_done" = "%@ ms";
"settings_contacts_carddav_deleted_toast" = "CardDAV-Konto gelöscht";
"settings_contacts_carddav_mandatory_field_not_filled_toast" = "Bitte geben Sie mindestens den Anzeigenamen und die Server-URL ein";
"settings_contacts_carddav_realm_title" = "Authentifizierungs-Realm";
"settings_contacts_carddav_sync_successful_toast" = "Synchronisierung erfolgreich";
"settings_contacts_carddav_use_as_default_title" = "Neu erstellte Kontakte hier speichern";
"settings_contacts_ldap_bind_user_password_title" = "Benutzerkennwort binden";
"settings_contacts_ldap_max_results_title" = "Maximale Ergebnisse";
"settings_contacts_ldap_request_timeout_title" = "Request timeout";
"settings_contacts_ldap_search_filter_title" = "Filtern";
"sip_address" = "SIP-Adresse";
"SRTP" = "SRTP";
"TCP" = "TCP";
"Temp Help" = "Temporärer-Hilfe";
"text_copied_to_clipboard_toast" = "Text in die Zwischenablage kopiert";
"TLS" = "TLS";
"UDP" = "UDP";
"uri_handler_bad_call_address_failed_toast" = "Anruf nicht möglich, ungültige Adresse";
"uri_handler_bad_config_address_failed_toast" = "Konfiguration konnte nicht abgerufen werden, ungültige Adresse";
"uri_handler_call_failed_toast" = "Anruf fehlgeschlagen";
"uri_handler_config_failed_toast" = "Konfiguration fehlgeschlagen";
"ZRTP" = "ZRTP";
"welcome_page_1_message" = "Eine **sichere** **Open Source** Kommunikations-App aus Frankreich.";
"welcome_page_3_message" = "Eine **kostenlose** Open-Source Anwendung seit **2001**.";
"help_about_contribute_translations_title" = "Zur Übersetzung von Linphone beitragen";
"help_about_privacy_policy_subtitle" = "Welche Informationen Linphone sammelt und nutzt";
"help_about_title" = "Über Linphone";
"help_about_user_guide_title" = "Linphone Benutzerhandbuch";

View file

@ -93,6 +93,7 @@
"assistant_third_party_sip_account_warning_explanation" = "Some features require a %@ account, such as group messaging, video conferences…\n\nThese features are hidden when you register with a third party SIP account.\n\nTo enable it in a commercial project, please contact us.";
"assistant_third_party_sip_account_warning_ok" = "I understand";
"assistant_web_platform_link" = "subscribe.linphone.org";
"authentication_id" = "Authentication ID (if different)";
"bottom_navigation_calls_label" = "Calls";
"bottom_navigation_contacts_label" = "Contacts";
"bottom_navigation_conversations_label" = "Conversations";

View file

@ -93,6 +93,7 @@
"assistant_third_party_sip_account_warning_explanation" = "Certaines fonctionnalités telles que les conversations de groupe, les vidéo-conférences, etc… nécessitent un compte %@.\n\nCes fonctionnalités seront masquées si vous utilisez un compte SIP tiers.\n\nPour les activer dans un projet commercial, merci de nous contacter.";
"assistant_third_party_sip_account_warning_ok" = "Jai compris";
"assistant_web_platform_link" = "subscribe.linphone.org";
"authentication_id" = "Identifiant de connexion (si différent)";
"bottom_navigation_calls_label" = "Appels";
"bottom_navigation_contacts_label" = "Contacts";
"bottom_navigation_conversations_label" = "Conversations";
@ -337,7 +338,7 @@
"manage_account_international_prefix" = "Indicatif international";
"manage_account_no_device" = "Aucun appareil n'a été trouvé…";
"manage_account_remove_picture" = "Supprimer";
"manage_account_settings" = "Mon compte";
"manage_account_settings" = "Paramètres";
"manage_account_status_cleared_summary" = "Compte désactivé, vous ne recevrez ni appel ni message.";
"manage_account_status_connected_summary" = "Vous êtes en ligne, on peut vous joindre.";
"manage_account_status_failed_summary" = "Erreur de connexion, vérifiez vos paramètres.";

View file

@ -0,0 +1,229 @@
"account_settings_title" = "Nastavenia účtu";
"account_settings_push_notification_not_available_title" = "Push notifikácie nie sú dostupné!";
"account_settings_im_encryption_mandatory_title" = "Šifrovanie správ je povinné";
"account_settings_sip_proxy_url_title" = "URL adresa SIP proxy servera";
"account_settings_outbound_proxy_title" = "Proxy server pre odchádzajúcu komunikáciu";
"account_settings_nat_policy_title" = "Nastavenia zásad NAT";
"account_settings_enable_ice_title" = "Povoliť ICE";
"account_settings_enable_turn_title" = "Povoliť TURN";
"account_settings_turn_username_title" = "TURN používateľské meno";
"account_settings_turn_password_title" = "TURN heslo";
"account_settings_avpf_title" = "AVPF (Profil audio-vizuálu so spätnou väzbou)";
"account_settings_expire_title" = "Platnosť (v sekundách)";
"account_settings_audio_video_conference_factory_uri_title" = "URI adresa pre audio/video hovory";
"account_settings_ccmp_server_url_title" = "RL adresa servera CCMP (Cisco CallManager Provisioning)";
"account_settings_lime_server_url_title" = "URL adresa servera pre kľúče koncového šifrovania";
"account_settings_bundle_mode_title" = "Režim zoskupenia";
"account_settings_mwi_uri_title" = "URI adresa servera MWI (Message Waiting Indicator)";
"account_settings_dialog_invalid_password_title" = "Vyžaduje sa overenie";
"bottom_navigation_contacts_label" = "Kontakty";
"bottom_navigation_calls_label" = "Hovory";
"account_settings_voicemail_uri_title" = "URI adresa hlasovej schránky";
"account_settings_update_password_title" = "Aktualizovať heslo";
"bottom_navigation_meetings_label" = "Schôdzky";
"contacts_list_empty" = "Momentálne žiadny kontakt…";
"contacts_list_favourites_title" = "Obľúbené";
"contacts_list_all_contacts_title" = "Všetky kontakty";
"drawer_menu_manage_account" = "Spravovať profil";
"drawer_menu_account_connection_status_connected" = "Pripojené";
"drawer_menu_account_connection_status_cleared" = "Zakázané";
"drawer_menu_account_connection_status_progress" = "Pripájanie…";
"drawer_menu_account_connection_status_failed" = "Chyba";
"drawer_menu_no_account_configured_yet" = "Žiadny účet zatiaľ nie je nastavený";
"drawer_menu_add_account" = "Pridať účet";
"help_about_check_for_update" = "Kontrola aktualizácie";
"help_about_advanced_title" = "Pokročilé";
"help_about_privacy_policy_title" = "Zásady ochrany súkromia";
"help_about_version_title" = "Verzia";
"help_version_up_to_date_toast_message" = "Vaša verzia je aktuálna";
"help_error_checking_version_toast_message" = "Počas kontroly aktualizácie nastala chyba";
"help_dialog_update_available_title" = "Je dostupná nová aktualizácia";
"help_quit_title" = "Ukončiť aplikáciu";
"help_troubleshooting_title" = "Riešenie problémov";
"help_troubleshooting_clean_logs" = "Vyčistiť záznamy";
"help_troubleshooting_print_logs_in_logcat" = "Zapisovať záznamy do logcatu";
"help_troubleshooting_share_logs" = "Zdieľať záznamy";
"help_troubleshooting_app_version_title" = "Verzia aplikácie";
"help_troubleshooting_sdk_version_title" = "Verzia SDK";
"help_troubleshooting_share_logs_dialog_title" = "Zdieľať odkaz na ladiace záznamy pomocou…";
"help_troubleshooting_debug_logs_cleaned_toast_message" = "Ladiace záznamy boli vyčistené";
"help_troubleshooting_debug_logs_upload_error_toast_message" = "Chyba pri nahrávaní ladiacich záznamov";
"help_troubleshooting_show_config_file" = "Zobraziť konfiguráciu";
"history_call_start_title" = "Nový hovor";
"history_call_start_search_bar_filter_hint" = "Hľadať kontakt alebo históriu hovoru";
"history_call_start_create_group_call" = "Vytvoriť skupinový hovor";
"history_group_call_start_dialog_set_subject" = "Nastaviť predmet skupinového hovoru";
"history_group_call_start_dialog_subject_hint" = "Predmet skupinového hovoru";
"history_dialog_delete_all_call_logs_title" = "Naozaj chcete zmazať celú históriu hovorov?";
"history_dialog_delete_all_call_logs_message" = "Z histórie budú odstránené všetky hovory";
"manage_account_details_title" = "Podrobnosti";
"manage_account_devices_title" = "Zariadenia";
"manage_account_edit_picture" = "Upraviť obrázok";
"manage_account_remove_picture" = "Odstrániť obrázok";
"manage_account_status_connected_summary" = "Tento účet je aktívny, každý Vám môže volať.";
"manage_account_international_prefix" = "Medzinárodný prefix";
"manage_account_settings" = "Nastavenia účtu";
"manage_account_delete" = "Odhlásiť sa";
"manage_account_device_last_connection" = "Posledné pripojenie:";
"manage_account_dialog_remove_account_title" = "Odhlásiť sa z Vášho účtu?";
"manage_account_dialog_remove_account_message" = "Pokiaľ si želáte nenávratne zmazať svoj účet, navštívte: https://sip.linphone.org";
"history_list_empty_history" = "Momentálne žiadny hovor…";
"manage_account_title" = "Spravovať účet";
"manage_account_status_failed_summary" = "Pripojenie účtu zlyhalo, skontrolujte nastavenia.";
"settings_advanced_title" = "Pokročilé nastavenia";
"settings_advanced_device_id_hint" = "Iba alfanumerické znaky";
"settings_advanced_upload_server_url" = "URL adresa servera pre zdieľanie súborov";
"settings_advanced_media_encryption_mandatory_title" = "Povinné šifrovanie médií";
"settings_advanced_accept_early_media_title" = "Prijímať zvuk pred spojením hovoru (early media)";
"settings_advanced_allow_outgoing_early_media_title" = "Prenášať zvuk pri odchádzajúcom hovore (early media)";
"settings_advanced_remote_provisioning_url" = "URL pre vzdialenú správu";
"settings_advanced_download_apply_remote_provisioning" = "Stiahnuť a použiť";
"settings_advanced_audio_devices_title" = "Zvukové zariadenia";
"settings_advanced_input_audio_device_title" = "Predvolené vstupné zvukové zariadenie";
"settings_advanced_output_audio_device_title" = "Predvolené výstupné zvukové zariadenie";
"settings_advanced_audio_codecs_title" = "Zvukové kodeky";
"settings_calls_calibrate_echo_canceller_in_progress" = "prebieha";
"settings_calls_calibrate_echo_canceller_done_no_echo" = "bez ozveny";
"settings_calls_calibrate_echo_canceller_failed" = "zlyhalo";
"settings_calls_adaptive_rate_control_title" = "Adaptívny kontrola rýchlosti";
"settings_calls_change_ringtone_title" = "Zmeniť vyzváňací tón";
"settings_advanced_video_codecs_title" = "Video kodeky";
"settings_calls_enable_video_title" = "Povoliť video";
"settings_calls_enable_fec_title" = "Povoliť FEC pre video";
"settings_calls_vibrate_while_ringing_title" = "Vibrovať počas prichádzajúceho hovoru";
"settings_contacts_add_ldap_server_title" = "Pridať LDAP server";
"settings_contacts_add_carddav_server_title" = "Pridať adresár CardDAV";
"settings_contacts_carddav_server_url_title" = "URL adresa servera";
"settings_contacts_carddav_sync_error_toast" = "Synchronizácia zlyhala!";
"settings_contacts_edit_ldap_server_title" = "Upraviť LDAP server";
"settings_contacts_edit_carddav_server_title" = "Upraviť adresár CardDAV";
"settings_contacts_ldap_bind_dn_title" = "Bind DN (pripájací identifikátor)";
"settings_title" = "Nastavenia";
"settings_security_title" = "Zabezpečenie";
"settings_security_enable_vfs_title" = "Šifrovať všetko";
"settings_security_enable_vfs_subtitle" = "Varovanie: po zapnutí sa už nedá zrušiť!";
"settings_security_prevent_screenshots_title" = "Zabrániť nahrávaniu rozhrania aplikácie";
"settings_conversations_auto_download_title" = "Automaticky sťahovať súbory";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Označiť konverzáciu ako prečítanú pri zavretí oznámenia o správe";
"settings_contacts_title" = "Kontakty";
"settings_contacts_ldap_use_tls_title" = "Použiť TLS";
"settings_contacts_ldap_server_url_title" = "URL adresa servera (nesmie byť prázdne)";
"settings_meetings_title" = "Schôdzky";
"settings_meetings_default_layout_title" = "Predvolené rozloženie";
"settings_meetings_layout_active_speaker_label" = "Aktívny hovoriaci";
"settings_meetings_layout_mosaic_label" = "Mozaika";
"settings_network_title" = "Sieť";
"settings_network_use_wifi_only" = "Používať iba siete Wi-Fi";
"settings_network_allow_ipv6" = "Povoliť IPv6";
"settings_conversations_title" = "Konverzácie";
"help_troubleshooting_clear_native_friends_in_database" = "Vymazať importované kontakty zo systémového adresára";
"manage_account_status_cleared_summary" = "Účet bol zakázaný, nebudete môcť prijímať hovory ani správy.";
"account_settings_push_notification_title" = "Povoliť push notifikácie";
"bottom_navigation_conversations_label" = "Konverzácie";
"account_settings_stun_server_url_title" = "URL adresa servera STUN/TURN";
"settings_calls_echo_canceller_title" = "Použiť softvérové potlačenie ozveny";
"help_title" = "Pomoc";
"settings_calls_auto_record_title" = "Automaticky spustiť nahrávanie hovorov";
"help_about_user_guide_subtitle" = "Naučte sa krok za krokom ovládať všetky funkcie aplikácie.";
"help_troubleshooting_firebase_project_title" = "ID Firebase project";
"manage_account_status_progress_summary" = "Účet sa pripája k serveru, prosím, čakajte…";
"settings_calls_title" = "Hovory";
"settings_calls_echo_canceller_subtitle" = "Zabraňuje, aby ozvenu bolo počuť na vzdialenej strane, ak nie je k dispozícii hardvérové potlačenie ozveny";
"settings_calls_calibrate_echo_canceller_title" = "Kalibrovať potlačenie ozveny";
"manage_account_add_picture" = "Pridať obrázok";
"account_settings_cpim_in_basic_conversations_title" = "Použiť CPIM v \"základných\" konverzáciách";
"account_settings_conference_factory_uri_title" = "URI adresa pre vytváranie konferencií";
"manage_account_device_remove" = "Odstrániť";
"welcome_page_2_title" = "Zabezpečená";
"welcome_page_3_title" = "Otvorená";
"welcome_page_title" = "Vitajte";
"account_settings_dialog_invalid_password_hint" = "Heslo";
"assistant_account_create" = "Vytvoriť";
"assistant_account_creation_wrong_phone_number" = "Nesprávne číslo?";
"assistant_account_login" = "Prihlásenie";
"assistant_account_login_forbidden_error" = "Nesprávne používateľské meno alebo heslo";
"assistant_account_register" = "Registrácia";
"assistant_account_register_push_notification_not_received_error" = "Push notifikácia s autentifikačným tokenom nebola prijatá behom 5 sekúnd, skúste to, prosím, neskôr znovu";
"assistant_account_register_unexpected_error" = "Nastala neočakávaná chyba, skúste to, prosím, neskôr znovu";
"assistant_already_have_an_account" = "Máte už účet?";
"assistant_create_account_using_email_on_our_web_platform" = "Vytvorte účet pomocou svojej e-mailovej adresy na:";
"assistant_dialog_confirm_phone_number_title" = "Potvrdiť telefónne číslo";
"assistant_dialog_general_terms_and_privacy_policy_title" = "Všeobecné podmienky a zásady ochrany súkromia";
"assistant_dialog_general_terms_label" = "všeobecné podmienky";
"assistant_dialog_privacy_policy_label" = "zásady ochrany súkromia";
"assistant_login_third_party_sip_account" = "Použiť SIP účet tretej strany";
"assistant_no_account_yet" = "Nemáte ešte účet?";
"assistant_permissions_grant_all_of_them" = "OK";
"assistant_permissions_skip_permissions" = "Vykonať neskôr";
"assistant_permissions_title" = "Udeliť oprávnenia";
"assistant_qr_code_invalid_toast" = "Neplatný QR kód!";
"assistant_scan_qr_code" = "Naskenovať QR kód";
"assistant_sip_account_transport_protocol" = "Prenos";
"assistant_third_party_sip_account_warning_ok" = "Rozumiem";
"contact_call_action" = "Volať";
"contact_details_delete" = "Vymazať";
"conversation_action_call" = "Volať";
"conversation_action_mark_as_read" = "Označiť ako prečítané";
"dialog_accept" = "Prijať";
"dialog_call" = "Volať";
"dialog_cancel" = "Zrušiť";
"dialog_continue" = "Pokračovať";
"dialog_deny" = "Odmietnuť";
"dialog_install" = "Nahrať";
"dialog_no" = "Nie";
"dialog_ok" = "OK";
"dialog_yes" = "Áno";
"meeting_waiting_room_cancel" = "Zrušiť";
"menu_delete_selected_item" = "Vymazať";
"menu_reply_to_chat_message" = "Odpovedať";
"next" = "Ďalej";
"notification_missed_call_title" = "Zmeškaný hovor";
"or" = "alebo";
"password" = "Heslo";
"phone_number" = "Telefónne číslo";
"settings_advanced_device_id" = "ID zariadenia";
"settings_contacts_carddav_name_title" = "Zobrazované meno";
"settings_contacts_carddav_password_title" = "Heslo";
"settings_contacts_carddav_username_title" = "Používateľské meno";
"settings_contacts_ldap_password_title" = "Heslo";
"sip_address_copied_to_clipboard_toast" = "SIP adresa skopírovaná do schránky";
"sip_address_display_name" = "Zobrazované meno";
"sip_address_domain" = "Doména";
"start" = "Začať";
"uri_handler_config_success_toast" = "Konfigurácia bola úspešne nastavená";
"username" = "Používateľské meno";
"contacts_list_filter_popup_see_all" = "Zobraziť všetko";
"contact_new_title" = "Nový kontakt";
"contact_edit_title" = "Upraviť kontakt";
"contact_editor_first_name" = "Krstné meno";
"contact_editor_last_name" = "Priezvisko";
"contact_editor_company" = "Spoločnosť";
"contact_editor_job_title" = "Pracovná pozícia";
"contact_editor_dialog_abort_confirmation_title" = "Neuložiť zmeny?";
"contact_editor_dialog_abort_confirmation_message" = "Všetky zmeny budú stratené";
"contact_details_actions_title" = "Ďalšie akcie";
"contact_details_edit" = "Upraviť";
"contact_details_add_to_favourites" = "Pridať do obľúbených";
"contact_details_remove_from_favourites" = "Odstrániť z obľúbených";
"contact_details_share" = "Zdieľať";
"contact_dialog_delete_message" = "Tento kontakt bude definitívne odstránený.";
"contact_dialog_pick_phone_number_or_sip_address_title" = "Vyberte číslo alebo SIP adresu";
"contact_message_action" = "Správa";
"contact_video_call_action" = "Videohovor";
"conversation_action_mute" = "Stlmiť";
"conversation_action_unmute" = "Zrušiť stlmenie";
"conversation_action_delete" = "Vymazať konverzáciu";
"conversation_action_leave_group" = "Opustiť skupinu";
"conversation_ephemeral_messages_title" = "Dočasné (miznúce) správy";
"conversations_list_empty" = "Momentálne žiadna konverzácia…";
"conversation_action_configure_ephemeral_messages" = "Nastavenie dočasných (miznúcich) správ";
"conference_layout_grid" = "Mozaika";
"conversation_ephemeral_messages_duration_disabled" = "Zakázané";
"conversation_menu_configure_ephemeral_messages" = "Dočasné (miznúce) správy";
"Error" = "Chyba";
"Interoperable mode" = "Režim vzájomnej kompatibility";
"manage_account_no_device" = "Zariadenie sa nenašlo…";
"message_delivery_info_error_title" = "Chyba";
"call_action_start_new_call" = "Nový hovor";
"call_stats_media_encryption_title" = "Šifrovanie médií";
"settings_contacts_ldap_search_base_title" = "Počiatočný bod hľadania (nesmie byť prázdne)";

View file

@ -30,10 +30,7 @@
<entry name="protocols" overwrite="true">stun,ice</entry>
</section>
<section name="sip">
<entry name="media_encryption" overwrite="true">zrtp</entry>
<entry name="media_encryption" overwrite="true">srtp</entry>
<entry name="media_encryption_mandatory">1</entry>
</section>
<section name="net">
<entry name="friendlist_subscription_enabled" overwrite="true">1</entry>
</section>
</config>

View file

@ -18,6 +18,7 @@
<entry name="conference_factory_uri" overwrite="true"></entry>
<entry name="audio_video_conference_factory_uri" overwrite="true"></entry>
<entry name="push_notification_allowed" overwrite="true">0</entry>
<entry name="remote_push_notification_allowed" overwrite="true">0</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">0</entry>
<entry name="rtp_bundle" overwrite="true">0</entry>
<entry name="lime_server_url" overwrite="true"></entry>

View file

@ -13,6 +13,7 @@ media_encryption=none
update_presence_model_timestamp_before_publish_expires_refresh=1
use_rfc2833=1
use_info=1
rls_uri=sips:rls@sip.linphone.org
[net]
#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit"

View file

@ -22,20 +22,17 @@ zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_MLK512,MS_ZRTP_KEY_AGREEME
chat_messages_aggregation_delay=1000
chat_messages_aggregation=1
update_presence_model_timestamp_before_publish_expires_refresh=1
rls_uri=sips:rls@sip.linphone.org
[sound]
#remove this property for any application that is not Linphone public version itself
ec_calibrator_cool_tones=1
disable_ringing=1
disable_ringing=0
[audio]
[video]
auto_resize_preview_to_keep_ratio=1
max_conference_size=vga
automatically_accept=1
automatically_initiate=0
[misc]
enable_basic_to_client_group_chat_room_migration=0
@ -49,6 +46,7 @@ record_aware=1
[account_creator]
url=https://subscribe.linphone.org/api/
backend=1
[lime]
lime_update_threshold=86400

View file

@ -342,11 +342,13 @@ class TelecomManager: ObservableObject {
}
func terminateCall(call: Call) {
do {
try call.terminate()
Log.info("Call terminated")
} catch {
Log.error("Failed to terminate call failed because \(error)")
CoreContext.shared.doOnCoreQueue { _ in
do {
try call.terminate()
Log.info("Call terminated")
} catch {
Log.error("Failed to terminate call failed because \(error)")
}
}
}

View file

@ -18,12 +18,14 @@
*/
import SwiftUI
import Combine
struct LoginFragment: View {
@ObservedObject private var coreContext = CoreContext.shared
@StateObject private var accountLoginViewModel = AccountLoginViewModel()
@StateObject private var keyboard = KeyboardResponder()
@State private var isSecured: Bool = true
@ -37,6 +39,8 @@ struct LoginFragment: View {
@State private var isLinkSIPActive = false
@State private var isLinkREGActive = false
@State var isShowHelpFragment = false
var isShowBack = false
var onBackPressed: (() -> Void)?
@ -93,6 +97,14 @@ struct LoginFragment: View {
}
}
if isShowHelpFragment {
HelpFragment(
isShowHelpFragment: $isShowHelpFragment
)
.transition(.move(edge: .trailing))
.zIndex(3)
}
if coreContext.loggingInProgress {
PopupLoadingView()
.background(.black.opacity(0.65))
@ -129,6 +141,26 @@ struct LoginFragment: View {
}
Spacer()
Button {
withAnimation {
isShowHelpFragment = true
}
} label: {
HStack {
Image("question")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 20, height: 20)
Text("help_title")
.foregroundStyle(Color.grayMain2c500)
.default_text_style_orange_600(styleSize: 15)
.frame(height: 35)
}
.padding(.horizontal, 20)
}
}
Text("assistant_account_login")
@ -313,7 +345,7 @@ struct LoginFragment: View {
.foregroundStyle(Color.grayMain2c700)
.padding(.horizontal, 10)
NavigationLink(destination: RegisterFragment(registerViewModel: RegisterViewModel()), isActive: $isLinkREGActive, label: { Text("assistant_account_register")
NavigationLink(destination: RegisterFragment(), isActive: $isLinkREGActive, label: { Text("assistant_account_register")
.default_text_style_white_600(styleSize: 20)
.frame(height: 35)
})
@ -347,6 +379,7 @@ struct LoginFragment: View {
.clipped()
}
.frame(minHeight: geometry.size.height)
.padding(.bottom, keyboard.currentHeight)
}
func acceptGeneralTerms() {

View file

@ -22,10 +22,12 @@
import SwiftUI
struct RegisterFragment: View {
@ObservedObject var registerViewModel: RegisterViewModel
@ObservedObject var sharedMainViewModel = SharedMainViewModel.shared
@StateObject private var registerViewModel = RegisterViewModel()
@StateObject private var keyboard = KeyboardResponder()
@Environment(\.dismiss) var dismiss
@State private var isSecured: Bool = true
@ -269,13 +271,13 @@ struct RegisterFragment: View {
})
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background((registerViewModel.username.isEmpty || registerViewModel.phoneNumber.isEmpty || registerViewModel.passwd.isEmpty) ? Color.orangeMain100 : Color.orangeMain500)
.background((registerViewModel.username.isEmpty || registerViewModel.dialPlanValueSelected == "---" || registerViewModel.phoneNumber.isEmpty || registerViewModel.passwd.isEmpty) ? Color.orangeMain100 : Color.orangeMain500)
.cornerRadius(60)
.disabled(!registerViewModel.isLinkActive)
.padding(.bottom)
.simultaneousGesture(
TapGesture().onEnded {
if !(registerViewModel.username.isEmpty || registerViewModel.phoneNumber.isEmpty || registerViewModel.passwd.isEmpty) {
if !(registerViewModel.username.isEmpty || registerViewModel.dialPlanValueSelected == "---" || registerViewModel.phoneNumber.isEmpty || registerViewModel.passwd.isEmpty) {
withAnimation {
self.isShowPopup = true
}
@ -347,11 +349,8 @@ struct RegisterFragment: View {
.clipped()
}
.frame(minHeight: geometry.size.height)
.padding(.bottom, keyboard.currentHeight)
}
}
#Preview {
RegisterFragment(registerViewModel: RegisterViewModel())
}
// swiftlint:enable line_length

View file

@ -24,25 +24,61 @@ struct ThirdPartySipAccountLoginFragment: View {
@ObservedObject private var coreContext = CoreContext.shared
@ObservedObject var accountLoginViewModel: AccountLoginViewModel
@StateObject private var keyboard = KeyboardResponder()
@Environment(\.dismiss) var dismiss
@State private var isSecured: Bool = true
@State private var advancedSettingsIsOpen: Bool = false
@FocusState var isNameFocused: Bool
@FocusState var isPasswordFocused: Bool
@FocusState var isDomainFocused: Bool
@FocusState var isDisplayNameFocused: Bool
@FocusState var isSipProxyUrlFocused: Bool
@FocusState var isAuthIdFocused: Bool
@FocusState var isOutboundProxyFocused: Bool
var body: some View {
GeometryReader { geometry in
if #available(iOS 16.4, *) {
ScrollView(.vertical) {
innerScrollView(geometry: geometry)
}
.scrollBounceBehavior(.basedOnSize)
} else {
ScrollView(.vertical) {
innerScrollView(geometry: geometry)
ScrollViewReader { proxy in
if #available(iOS 16.4, *) {
ScrollView(.vertical) {
innerScrollView(geometry: geometry)
}
.scrollBounceBehavior(.basedOnSize)
.onChange(of: isAuthIdFocused) { field in
if field {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
proxy.scrollTo(2, anchor: .top)
}
}
}
.onChange(of: isOutboundProxyFocused) { field in
if field {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
proxy.scrollTo(2, anchor: .top)
}
}
}
} else {
ScrollView(.vertical) {
innerScrollView(geometry: geometry)
}
.onChange(of: isAuthIdFocused) { field in
if field {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
proxy.scrollTo(2, anchor: .top)
}
}
}
.onChange(of: isOutboundProxyFocused) { field in
if field {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
proxy.scrollTo(2, anchor: .top)
}
}
}
}
}
}
@ -208,6 +244,74 @@ struct ThirdPartySipAccountLoginFragment: View {
.stroke(Color.gray200, lineWidth: 1)
)
.padding(.bottom)
HStack(alignment: .center) {
Text("settings_advanced_title")
.default_text_style_800(styleSize: 18)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
Image(advancedSettingsIsOpen ? "caret-up" : "caret-down")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
.padding(.top, 10)
.padding(.bottom, 10)
.background(.white)
.onTapGesture {
withAnimation {
advancedSettingsIsOpen.toggle()
}
}
if advancedSettingsIsOpen {
VStack(alignment: .leading) {
Text("authentication_id")
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
TextField("authentication_id", text: $accountLoginViewModel.authId)
.id(1)
.default_text_style(styleSize: 15)
.frame(height: 25)
.padding(.horizontal, 20)
.padding(.vertical, 15)
.background(.white)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(isAuthIdFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
)
.focused($isAuthIdFocused)
}
VStack(alignment: .leading) {
Text("account_settings_sip_proxy_url_title")
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
TextField("account_settings_sip_proxy_url_title", text: $accountLoginViewModel.outboundProxy)
.id(2)
.default_text_style(styleSize: 15)
.frame(height: 25)
.padding(.horizontal, 20)
.padding(.vertical, 15)
.background(.white)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(isOutboundProxyFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
)
.focused($isOutboundProxyFocused)
}
.padding(.bottom)
}
}
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
.padding(.horizontal, 20)
@ -241,6 +345,7 @@ struct ThirdPartySipAccountLoginFragment: View {
.clipped()
}
.frame(minHeight: geometry.size.height)
.padding(.bottom, keyboard.currentHeight)
}
}

View file

@ -29,6 +29,8 @@ class AccountLoginViewModel: ObservableObject {
@Published var domain: String = "sip.linphone.org"
@Published var displayName: String = ""
@Published var transportType: String = "TLS"
@Published var authId: String = ""
@Published var outboundProxy: String = ""
private var mCoreDelegate: CoreDelegate!
@ -83,7 +85,7 @@ class AccountLoginViewModel: ObservableObject {
// The realm will be determined automatically from the first register, as well as the algorithm
let authInfo = try Factory.Instance.createAuthInfo(
username: self.username,
userid: "",
userid: self.authId,
passwd: self.passwd,
ha1: "",
realm: "",
@ -100,15 +102,26 @@ class AccountLoginViewModel: ObservableObject {
try accountParams.setIdentityaddress(newValue: identity)
// We also need to configure where the proxy server is located
let address = try Factory.Instance.createAddress(addr: String("sip:" + self.domain))
var serverAddress: Address
if (!self.outboundProxy.isEmpty) {
let server = self.outboundProxy.starts(with: "sip:") ? self.outboundProxy : String("sip:" + self.outboundProxy)
serverAddress = try Factory.Instance.createAddress(addr: server)
} else {
serverAddress = try Factory.Instance.createAddress(addr: String("sip:" + self.domain))
}
let address = serverAddress
// We use the Address object to easily set the transport protocol
try address.setTransport(newValue: transport)
try accountParams.setServeraddress(newValue: address)
// And we ensure the account will start the registration process
accountParams.registerEnabled = true
accountParams.pushNotificationAllowed = true
accountParams.remotePushNotificationAllowed = true
if accountParams.pushNotificationAllowed {
accountParams.pushNotificationAllowed = true
accountParams.remotePushNotificationAllowed = true
}
#if DEBUG
let pushEnvironment = ".dev"
#else
@ -116,10 +129,6 @@ class AccountLoginViewModel: ObservableObject {
#endif
accountParams.pushNotificationConfig?.provider = "apns" + pushEnvironment
accountParams.internationalPrefix = "33"
accountParams.internationalPrefixIsoCountryCode = "FRA"
accountParams.useInternationalPrefixForCallsAndChats = true
self.mCoreDelegate = CoreDelegateStub(onAccountRegistrationStateChanged: { (core: Core, account: Account, state: RegistrationState, message: String) in
Log.info("New registration state is \(state) for user id " +
@ -153,6 +162,8 @@ class AccountLoginViewModel: ObservableObject {
DispatchQueue.main.async {
self.domain = "sip.linphone.org"
self.transportType = "TLS"
self.authId = ""
self.outboundProxy = ""
}
} catch { NSLog(error.localizedDescription) }

View file

@ -41,7 +41,7 @@ class RegisterViewModel: ObservableObject {
@Published var displayName: String = ""
@Published var transportType: String = "TLS"
@Published var dialPlanValueSelected: String = "🇫🇷 +33"
@Published var dialPlanValueSelected: String = "---"
private let HASHALGORITHM = "SHA-256"
@ -257,7 +257,7 @@ class RegisterViewModel: ObservableObject {
SharedMainViewModel.shared.dialPlansList.forEach { dial in
let countryCode = dialPlanValueSelected.components(separatedBy: "+")
if dial.countryCallingCode == countryCode[1] {
if dial?.countryCallingCode == countryCode[1] {
dialPlan = dial
}
}
@ -412,7 +412,7 @@ class RegisterViewModel: ObservableObject {
for dial in SharedMainViewModel.shared.dialPlansList {
let countryCode = self.dialPlanValueSelected.components(separatedBy: "+")
if dial.countryCallingCode == countryCode[1] {
if dial?.countryCallingCode == countryCode[1] {
dialPlan = dial
break
}

View file

@ -347,7 +347,7 @@ struct CallView: View {
.padding(.leading, 50)
.padding(.top, 35)
Text("call_zrtp_end_to_end_encrypted")
Text(callViewModel.isConference ? "call_srtp_point_to_point_encrypted" : "call_zrtp_end_to_end_encrypted")
.foregroundStyle(Color.blueInfo500)
.default_text_style_white(styleSize: 12)
.padding(.top, 35)
@ -558,6 +558,10 @@ struct CallView: View {
}
}
.onDisappear {
coreContext.doOnCoreQueue { core in
core.nativeVideoWindow = nil
}
if callViewModel.videoDisplayed {
if !callViewModel.isPaused && TelecomManager.shared.callInProgress
&& !(coreContext.pipViewModel.pipController?.isPictureInPictureActive ?? false) {
@ -585,6 +589,11 @@ struct CallView: View {
core.nativePreviewWindow = view
}
}
.onDisappear {
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = nil
}
}
.aspectRatio(callViewModel.callStatsModel.sentVideoWindow.widthFactor/callViewModel.callStatsModel.sentVideoWindow.heightFactor, contentMode: .fill)
.frame(maxWidth: callViewModel.callStatsModel.sentVideoWindow.widthFactor * 256,
maxHeight: callViewModel.callStatsModel.sentVideoWindow.heightFactor * 256)
@ -703,6 +712,11 @@ struct CallView: View {
core.nativePreviewWindow = view
}
}
.onDisappear {
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = nil
}
}
.aspectRatio(callViewModel.callStatsModel.sentVideoWindow.widthFactor/callViewModel.callStatsModel.sentVideoWindow.heightFactor, contentMode: .fill)
.frame(maxWidth: callViewModel.callStatsModel.sentVideoWindow.widthFactor * 256,
maxHeight: callViewModel.callStatsModel.sentVideoWindow.heightFactor * 256)
@ -897,6 +911,9 @@ struct CallView: View {
}
}
.onDisappear {
coreContext.doOnCoreQueue { core in
core.nativeVideoWindow = nil
}
if !callViewModel.isPaused && TelecomManager.shared.callInProgress
&& !(coreContext.pipViewModel.pipController?.isPictureInPictureActive ?? false) {
// TODO: Enable PIP in 6.1
@ -978,6 +995,11 @@ struct CallView: View {
core.nativePreviewWindow = view
}
}
.onDisappear {
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = nil
}
}
.frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2)
.scaledToFill()
.clipped()
@ -1143,6 +1165,11 @@ struct CallView: View {
core.nativePreviewWindow = view
}
}
.onDisappear {
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = nil
}
}
.frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2)
.scaledToFill()
.clipped()
@ -1362,6 +1389,11 @@ struct CallView: View {
core.nativePreviewWindow = view
}
}
.onDisappear {
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = nil
}
}
.frame(
width: 120 * ceil(maxValue / 120),
height: 160 * ceil(maxValue / 120)
@ -1598,6 +1630,11 @@ struct CallView: View {
core.nativePreviewWindow = view
}
}
.onDisappear {
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = nil
}
}
.frame(
width: 160 * ceil(maxValue / 120),
height: 120 * ceil(maxValue / 120)
@ -1938,9 +1975,9 @@ struct CallView: View {
.frame(width: buttonSize, height: buttonSize)
.background(Color.gray500)
.cornerRadius(40)
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote || telecomManager.outgoingCallStarted)
if callViewModel.isPaused || telecomManager.isPausedByRemote {
if callViewModel.isPaused || telecomManager.isPausedByRemote || telecomManager.outgoingCallStarted {
Color.gray600.opacity(0.8)
.cornerRadius(40)
.allowsHitTesting(false)
@ -2219,7 +2256,7 @@ struct CallView: View {
changeLayoutSheet = true
} label: {
HStack {
Image("notebook")
Image("layout")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
@ -2648,7 +2685,7 @@ struct CallView: View {
changeLayoutSheet = true
} label: {
HStack {
Image("notebook")
Image("layout")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)

View file

@ -713,44 +713,45 @@ class CallViewModel: ObservableObject {
func displayMyVideo() {
coreContext.doOnCoreQueue { core in
if self.currentCall != nil {
do {
let params = try core.createCallParams(call: self.currentCall)
if (params.videoEnabled == false) {
Log.info("\(CallViewModel.TAG) Conference found and video disabled in params, enabling it")
params.videoEnabled = true
params.videoDirection = MediaDirection.SendRecv
guard let call = self.currentCall else { return }
guard call.state == .StreamsRunning else {
Log.warn("\(CallViewModel.TAG) displayMyVideo called in invalid state: \(call.state), skipping update")
return
}
do {
let params = try core.createCallParams(call: call)
if !params.videoEnabled {
Log.info("\(CallViewModel.TAG) Video disabled in params, enabling it")
params.videoEnabled = true
params.videoDirection = .SendRecv
} else {
if params.videoDirection == .SendRecv || params.videoDirection == .SendOnly {
Log.info("\(CallViewModel.TAG) Video already enabled, switching to recv only")
params.videoDirection = .RecvOnly
} else {
if (params.videoDirection == MediaDirection.SendRecv || params.videoDirection == MediaDirection.SendOnly) {
Log.info(
"\(CallViewModel.TAG) Conference found with video already enabled, changing video media direction to receive only"
)
params.videoDirection = MediaDirection.RecvOnly
} else {
Log.info(
"\(CallViewModel.TAG) Conference found with video already enabled, changing video media direction to send & receive"
)
params.videoDirection = MediaDirection.SendRecv
}
Log.info("\(CallViewModel.TAG) Video already enabled, switching to send & recv")
params.videoDirection = .SendRecv
}
try self.currentCall!.update(params: params)
let video = params.videoDirection == .SendRecv || params.videoDirection == .SendOnly
DispatchQueue.main.asyncAfter(deadline: .now() + (video ? 1 : 0)) {
if video {
self.videoDisplayed = false
}
self.videoDisplayed = video
}
} catch {
}
try call.update(params: params)
let video = params.videoDirection == .SendRecv || params.videoDirection == .SendOnly
DispatchQueue.main.asyncAfter(deadline: .now() + (video ? 1 : 0)) {
if video {
self.videoDisplayed = false
}
self.videoDisplayed = video
}
} catch {
Log.error("\(CallViewModel.TAG) Failed to update video params: \(error)")
}
}
}
func toggleVideoMode(isAudioOnlyMode: Bool) {
coreContext.doOnCoreQueue { core in
@ -985,14 +986,11 @@ class CallViewModel: ObservableObject {
self.isNotEncrypted = false
}
case MediaEncryption.None:
let isNotEncryptedTmp = self.currentCall?.state == .StreamsRunning
DispatchQueue.main.async {
self.isMediaEncrypted = false
self.isZrtp = false
if self.currentCall!.state == .StreamsRunning {
self.isNotEncrypted = true
} else {
self.isNotEncrypted = false
}
self.isNotEncrypted = isNotEncryptedTmp
}
}
}

View file

@ -29,6 +29,7 @@ struct EditContactFragment: View {
@State private var orientation = UIDevice.current.orientation
@StateObject private var editContactViewModel: EditContactViewModel
@StateObject private var keyboard = KeyboardResponder()
@Binding var isShowEditContactFragment: Bool
@Binding var isShowDismissPopup: Bool
@ -100,8 +101,8 @@ struct EditContactFragment: View {
if editContactViewModel.selectedEditFriend == nil
&& editContactViewModel.firstName.isEmpty
&& editContactViewModel.lastName.isEmpty
&& editContactViewModel.sipAddresses.first!.isEmpty
&& editContactViewModel.phoneNumbers.first!.isEmpty
&& editContactViewModel.sipAddresses.first?.isEmpty ?? true
&& editContactViewModel.phoneNumbers.first?.isEmpty ?? true
&& editContactViewModel.company.isEmpty
&& editContactViewModel.jobTitle.isEmpty {
delayColorDismiss()
@ -113,8 +114,8 @@ struct EditContactFragment: View {
} else {
if editContactViewModel.firstName.isEmpty
&& editContactViewModel.lastName.isEmpty
&& editContactViewModel.sipAddresses.first!.isEmpty
&& editContactViewModel.phoneNumbers.first!.isEmpty
&& editContactViewModel.sipAddresses.first?.isEmpty ?? true
&& editContactViewModel.phoneNumbers.first?.isEmpty ?? true
&& editContactViewModel.company.isEmpty
&& editContactViewModel.jobTitle.isEmpty {
withAnimation {
@ -318,7 +319,6 @@ struct EditContactFragment: View {
.padding(.bottom, -5)
ForEach(editContactViewModel.sipAddresses.indices, id: \.self) { index in
HStack(alignment: .center) {
TextField("sip_address", text: $editContactViewModel.sipAddresses[index])
.default_text_style(styleSize: 15)
@ -336,27 +336,27 @@ struct EditContactFragment: View {
)
.focused($isSIPAddressFocused, equals: index)
.onChange(of: editContactViewModel.sipAddresses[index]) { newValue in
if !newValue.isEmpty && index + 1 == editContactViewModel.sipAddresses.count {
if !newValue.isEmpty && index == editContactViewModel.sipAddresses.count - 1 {
editContactViewModel.sipAddresses.append("")
}
}
Button(action: {
guard editContactViewModel.sipAddresses.indices.contains(index) else { return }
editContactViewModel.sipAddresses.remove(at: index)
}, label: {
}) {
Image("x")
.renderingMode(.template)
.resizable()
.foregroundStyle(
editContactViewModel.sipAddresses[index].isEmpty && editContactViewModel.sipAddresses.count == index + 1
editContactViewModel.sipAddresses[index].isEmpty && index == editContactViewModel.sipAddresses.count - 1
? Color.gray100
: Color.grayMain2c600
)
.frame(width: 25, height: 25)
.padding(.all, 10)
})
.disabled(editContactViewModel.sipAddresses[index].isEmpty && editContactViewModel.sipAddresses.count == index + 1)
.frame(maxHeight: .infinity)
}
.disabled(editContactViewModel.sipAddresses[index].isEmpty && index == editContactViewModel.sipAddresses.count - 1)
}
}
}
@ -367,12 +367,12 @@ struct EditContactFragment: View {
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
ForEach(0..<editContactViewModel.phoneNumbers.count, id: \.self) { index in
ForEach(editContactViewModel.phoneNumbers.indices, id: \.self) { index in
HStack(alignment: .center) {
TextField("phone_number", text: $editContactViewModel.phoneNumbers[index])
.default_text_style(styleSize: 15)
.textContentType(.oneTimeCode)
.keyboardType(.numberPad)
.keyboardType(.phonePad)
.frame(height: 25)
.padding(.horizontal, 20)
.padding(.vertical, 15)
@ -385,7 +385,7 @@ struct EditContactFragment: View {
)
.focused($isPhoneNumberFocused, equals: index)
.onChange(of: editContactViewModel.phoneNumbers[index]) { newValue in
if !newValue.isEmpty && index + 1 == editContactViewModel.phoneNumbers.count {
if !newValue.isEmpty && index == editContactViewModel.phoneNumbers.count - 1 {
withAnimation {
editContactViewModel.phoneNumbers.append("")
}
@ -393,21 +393,21 @@ struct EditContactFragment: View {
}
Button(action: {
guard editContactViewModel.phoneNumbers.indices.contains(index) else { return }
editContactViewModel.phoneNumbers.remove(at: index)
}, label: {
}) {
Image("x")
.renderingMode(.template)
.resizable()
.foregroundStyle(
editContactViewModel.phoneNumbers[index].isEmpty && editContactViewModel.phoneNumbers.count == index + 1
editContactViewModel.phoneNumbers[index].isEmpty && index == editContactViewModel.phoneNumbers.count - 1
? Color.gray100
: Color.grayMain2c600
)
.frame(width: 25, height: 25)
.padding(.all, 10)
})
.disabled(editContactViewModel.phoneNumbers[index].isEmpty && editContactViewModel.phoneNumbers.count == index + 1)
.frame(maxHeight: .infinity)
}
.disabled(editContactViewModel.phoneNumbers[index].isEmpty && index == editContactViewModel.phoneNumbers.count - 1)
}
.zIndex(isPhoneNumberFocused == index ? 1 : 0)
.transition(.move(edge: .top))
@ -510,83 +510,90 @@ struct EditContactFragment: View {
organizationName: editContactViewModel.company,
jobTitle: editContactViewModel.jobTitle,
displayName: "",
sipAddresses: editContactViewModel.sipAddresses.map { $0 },
phoneNumbers: editContactViewModel.phoneNumbers.map { PhoneNumber(numLabel: "", num: $0)},
sipAddresses: editContactViewModel.sipAddresses,
phoneNumbers: editContactViewModel.phoneNumbers.map { PhoneNumber(numLabel: "", num: $0) },
imageData: ""
)
if editContactViewModel.selectedEditFriend != nil && editContactViewModel.selectedEditFriend!.friend != nil && selectedImage == nil &&
!removedImage && editContactViewModel.selectedEditFriend!.friend!.photo!.suffix(11) != "default.png" {
ContactsManager.shared.saveFriend(
result: String(editContactViewModel.selectedEditFriend!.friend!.photo!.dropFirst(6)),
contact: newContact,
existingFriend: editContactViewModel.selectedEditFriend!.friend, completion: {_ in
if let selectedFriendTmp = editContactViewModel.selectedEditFriend?.friend {
let addressTmp = selectedFriendTmp.address?.clone()?.asStringUriOnly() ?? ""
SharedMainViewModel.shared.displayedFriend?.resetContactAvatarModel(
friend: selectedFriendTmp,
name: selectedFriendTmp.name ?? "",
address: addressTmp,
withPresence: SharedMainViewModel.shared.displayedFriend?.withPresence
)
}
let friendIsNil = editContactViewModel.selectedEditFriend?.friend == nil
DispatchQueue.main.async {
delayColorDismiss()
if friendIsNil {
withAnimation {
isShowEditContactFragment.toggle()
}
} else {
withAnimation {
dismiss()
}
}
editContactViewModel.resetValues()
}
}
)
let existingFriend = editContactViewModel.selectedEditFriend?.friend
let friendHasCustomPhoto = existingFriend?.photo?.suffix(11) != "default.png"
// Case: editing existing friend without changing the image
if let existingFriend = existingFriend,
selectedImage == nil,
!removedImage,
friendHasCustomPhoto,
let photo = existingFriend.photo {
let resultPhoto = String(photo.dropFirst(6))
ContactsManager.shared.saveFriend(result: resultPhoto, contact: newContact, existingFriend: existingFriend) { _ in
self.updateAvatar(for: existingFriend)
self.finishUIUpdate(existingFriend: existingFriend)
}
} else {
ContactsManager.shared.saveImage(
image: selectedImage
?? ContactsManager.shared.textToImage(
firstName: editContactViewModel.firstName, lastName: editContactViewModel.lastName),
name: editContactViewModel.firstName
+ editContactViewModel.lastName,
prefix: ((selectedImage == nil) ? "-default" : ""),
contact: newContact, linphoneFriend: "Linphone address-book", existingFriend: editContactViewModel.selectedEditFriend?.friend) {
if let selectedFriendTmp = editContactViewModel.selectedEditFriend?.friend {
let addressTmp = selectedFriendTmp.address?.clone()?.asStringUriOnly() ?? ""
SharedMainViewModel.shared.displayedFriend?.resetContactAvatarModel(
friend: selectedFriendTmp,
name: selectedFriendTmp.name ?? "",
address: addressTmp,
withPresence: SharedMainViewModel.shared.displayedFriend?.withPresence
)
} else {
MagicSearchSingleton.shared.searchForContacts()
ContactsManager.shared.updateSubscriptionsLinphoneList()
}
let friendIsNil = editContactViewModel.selectedEditFriend?.friend == nil
DispatchQueue.main.async {
delayColorDismiss()
if friendIsNil {
withAnimation {
isShowEditContactFragment.toggle()
}
} else {
withAnimation {
dismiss()
}
}
editContactViewModel.resetValues()
}
}
// Case: creating new friend or updating with a new image
let imageToSave = selectedImage ?? ContactsManager.shared.textToImage(
firstName: editContactViewModel.firstName,
lastName: editContactViewModel.lastName
)
let prefix = selectedImage == nil ? "-default" : ""
saveImageThreadSafe(
image: imageToSave,
name: editContactViewModel.firstName + editContactViewModel.lastName,
prefix: prefix,
contact: newContact,
existingFriend: existingFriend,
linphoneFriend: "Linphone address-book"
)
}
}
}
private func saveImageThreadSafe(image: UIImage, name: String, prefix: String, contact: Contact, existingFriend: Friend?, linphoneFriend: String) {
ContactsManager.shared.saveImage(
image: image,
name: name,
prefix: prefix,
contact: contact,
linphoneFriend: linphoneFriend,
existingFriend: existingFriend
) {
if let existingFriend = existingFriend {
self.updateAvatar(for: existingFriend)
} else {
MagicSearchSingleton.shared.searchForContacts()
ContactsManager.shared.updateSubscriptionsLinphoneList()
}
self.finishUIUpdate(existingFriend: existingFriend)
}
}
private func updateAvatar(for friend: Friend) {
let addressTmp = friend.address?.clone()?.asStringUriOnly() ?? ""
SharedMainViewModel.shared.displayedFriend?.resetContactAvatarModel(
friend: friend,
name: friend.name ?? "",
address: addressTmp,
withPresence: SharedMainViewModel.shared.displayedFriend?.withPresence
)
}
private func finishUIUpdate(existingFriend: Friend?) {
let friendIsNil = existingFriend == nil
DispatchQueue.main.async {
delayColorDismiss()
withAnimation {
if friendIsNil {
isShowEditContactFragment.toggle()
} else {
dismiss()
}
}
}
editContactViewModel.resetValues()
}
}
#Preview {

View file

@ -346,32 +346,34 @@ struct ContentView: View {
}
.frame(height: geometry.size.height/4)
}
Button(action: {
sharedMainViewModel.changeIndexView(indexViewInt: 3)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
sharedMainViewModel.displayedConversation = nil
}, label: {
VStack {
Image("video-conference")
.renderingMode(.template)
.resizable()
.foregroundStyle(sharedMainViewModel.indexView == 3 ? Color.orangeMain500 : Color.grayMain2c600)
.frame(width: 25, height: 25)
if sharedMainViewModel.indexView == 0 {
Text("bottom_navigation_meetings_label")
.default_text_style_700(styleSize: 10)
} else {
Text("bottom_navigation_meetings_label")
.default_text_style(styleSize: 10)
}
}
})
.padding(.top)
.frame(height: geometry.size.height/4)
Spacer()
if !sharedMainViewModel.disableMeetingFeature {
Button(action: {
sharedMainViewModel.changeIndexView(indexViewInt: 3)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
sharedMainViewModel.displayedConversation = nil
}, label: {
VStack {
Image("video-conference")
.renderingMode(.template)
.resizable()
.foregroundStyle(sharedMainViewModel.indexView == 3 ? Color.orangeMain500 : Color.grayMain2c600)
.frame(width: 25, height: 25)
if sharedMainViewModel.indexView == 0 {
Text("bottom_navigation_meetings_label")
.default_text_style_700(styleSize: 10)
} else {
Text("bottom_navigation_meetings_label")
.default_text_style(styleSize: 10)
}
}
})
.padding(.top)
.frame(height: geometry.size.height/4)
Spacer()
}
}
}
.frame(width: 75, height: geometry.size.height)
@ -405,6 +407,17 @@ struct ContentView: View {
VStack(spacing: 0) {
if searchIsActive == false {
HStack {
Button {
openMenu()
} label: {
Image("list")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 5)
}
if let index = accountProfileViewModel.defaultAccountModelIndex,
index < coreContext.accounts.count {
@ -473,7 +486,7 @@ struct ContentView: View {
Text(String(localized: sharedMainViewModel.indexView == 0 ? "bottom_navigation_contacts_label" : (sharedMainViewModel.indexView == 1 ? "bottom_navigation_calls_label" : (sharedMainViewModel.indexView == 2 ? "bottom_navigation_conversations_label" : "bottom_navigation_meetings_label"))))
.default_text_style_white_800(styleSize: 20)
.padding(.leading, 10)
.padding(.leading, 2)
Spacer()
@ -895,31 +908,33 @@ struct ContentView: View {
.frame(width: 66)
}
}
Spacer()
Button(action: {
sharedMainViewModel.changeIndexView(indexViewInt: 3)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
sharedMainViewModel.displayedConversation = nil
}, label: {
VStack {
Image("video-conference")
.renderingMode(.template)
.resizable()
.foregroundStyle(sharedMainViewModel.indexView == 3 ? Color.orangeMain500 : Color.grayMain2c600)
.frame(width: 25, height: 25)
if sharedMainViewModel.indexView == 3 {
Text("bottom_navigation_meetings_label")
.default_text_style_700(styleSize: 9)
} else {
Text("bottom_navigation_meetings_label")
.default_text_style(styleSize: 9)
if !sharedMainViewModel.disableMeetingFeature {
Spacer()
Button(action: {
sharedMainViewModel.changeIndexView(indexViewInt: 3)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedCall = nil
sharedMainViewModel.displayedConversation = nil
}, label: {
VStack {
Image("video-conference")
.renderingMode(.template)
.resizable()
.foregroundStyle(sharedMainViewModel.indexView == 3 ? Color.orangeMain500 : Color.grayMain2c600)
.frame(width: 25, height: 25)
if sharedMainViewModel.indexView == 3 {
Text("bottom_navigation_meetings_label")
.default_text_style_700(styleSize: 9)
} else {
Text("bottom_navigation_meetings_label")
.default_text_style(styleSize: 9)
}
}
}
})
.padding(.top)
.frame(width: 66)
})
.padding(.top)
.frame(width: 66)
}
Spacer()
}
@ -1047,17 +1062,22 @@ struct ContentView: View {
}
if isShowEditContactFragment {
EditContactFragment(
isShowEditContactFragment: $isShowEditContactFragment,
isShowDismissPopup: $isShowDismissPopup,
isShowEditContactFragmentAddress: isShowEditContactFragmentAddress
)
VStack {
EditContactFragment(
isShowEditContactFragment: $isShowEditContactFragment,
isShowDismissPopup: $isShowDismissPopup,
isShowEditContactFragmentAddress: isShowEditContactFragmentAddress
)
.frame(height: geometry.size.height)
.onAppear {
sharedMainViewModel.displayedFriend = nil
isShowEditContactFragmentAddress = ""
}
Spacer()
}
.zIndex(3)
.transition(.opacity.combined(with: .move(edge: .bottom)))
.onAppear {
sharedMainViewModel.displayedFriend = nil
isShowEditContactFragmentAddress = ""
}
}
if isShowStartCallFragment {

View file

@ -378,12 +378,15 @@ class ConversationViewModel: ObservableObject {
}
}
}, onEphemeralMessageTimerStarted: { (message: ChatMessage) in
let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLogId == message.messageId})
let ephemeralExpireTimeTmp = message.ephemeralExpireTime
DispatchQueue.main.async {
if indexMessage != nil {
self.conversationMessagesSection[0].rows[indexMessage!].message.ephemeralExpireTime = ephemeralExpireTimeTmp
if !self.conversationMessagesSection.isEmpty,
!self.conversationMessagesSection[0].rows.isEmpty,
let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: { $0.eventModel.eventLogId == message.messageId }),
indexMessage < self.conversationMessagesSection[0].rows.count {
let ephemeralExpireTimeTmp = message.ephemeralExpireTime
DispatchQueue.main.async {
self.conversationMessagesSection[0].rows[indexMessage].message.ephemeralExpireTime = ephemeralExpireTimeTmp
}
}
})
@ -2001,25 +2004,23 @@ class ConversationViewModel: ObservableObject {
}
func resetDisplayedChatRoom() {
if !self.conversationMessagesSection.isEmpty && !self.conversationMessagesSection[0].rows.isEmpty {
if let displayedConversation = self.sharedMainViewModel.displayedConversation {
CoreContext.shared.doOnCoreQueue { core in
let nilParams: ConferenceParams? = nil
if let newChatRoom = core.searchChatRoom(params: nilParams, localAddr: nil, remoteAddr: displayedConversation.chatRoom.peerAddress, participants: nil) {
if LinphoneUtils.getChatRoomId(room: newChatRoom) == displayedConversation.id {
self.addConversationDelegate(chatRoom: newChatRoom)
let conversation = ConversationModel(chatRoom: newChatRoom)
DispatchQueue.main.async {
self.sharedMainViewModel.displayedConversation = conversation
}
self.computeComposingLabel()
let historyEventsSizeTmp = newChatRoom.historyEventsSize
if self.displayedConversationHistorySize < historyEventsSizeTmp {
let eventLogList = newChatRoom.getHistoryRangeEvents(begin: 0, end: historyEventsSizeTmp - self.displayedConversationHistorySize)
if !eventLogList.isEmpty {
self.getNewMessages(eventLogs: eventLogList)
}
if let displayedConversation = self.sharedMainViewModel.displayedConversation {
CoreContext.shared.doOnCoreQueue { core in
let nilParams: ConferenceParams? = nil
if let newChatRoom = core.searchChatRoom(params: nilParams, localAddr: nil, remoteAddr: displayedConversation.chatRoom.peerAddress, participants: nil) {
if LinphoneUtils.getChatRoomId(room: newChatRoom) == displayedConversation.id {
self.addConversationDelegate(chatRoom: newChatRoom)
let conversation = ConversationModel(chatRoom: newChatRoom)
DispatchQueue.main.async {
self.sharedMainViewModel.displayedConversation = conversation
}
self.computeComposingLabel()
let historyEventsSizeTmp = newChatRoom.historyEventsSize
if self.displayedConversationHistorySize < historyEventsSizeTmp {
let eventLogList = newChatRoom.getHistoryRangeEvents(begin: 0, end: historyEventsSizeTmp - self.displayedConversationHistorySize)
if !eventLogList.isEmpty {
self.getNewMessages(eventLogs: eventLogList)
}
}
}

View file

@ -29,14 +29,21 @@ struct HelpFragment: View {
@FocusState var isVoicemailUriFocused: Bool
var showAssistant: Bool {
(CoreContext.shared.coreIsStarted && CoreContext.shared.accounts.isEmpty)
|| SharedMainViewModel.shared.displayProfileMode
}
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 1) {
Rectangle()
.foregroundColor(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
if !showAssistant {
Rectangle()
.foregroundColor(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
}
HStack {
Image("caret-left")
@ -302,5 +309,7 @@ struct HelpFragment: View {
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle("")
.navigationBarHidden(true)
}
}

View file

@ -100,13 +100,9 @@ struct DialerBottomSheet: View {
HStack {
Button {
if currentCall != nil {
do {
let digit = ("1".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "1"
} catch {
}
let digit = ("1".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "1"
} else {
startCallViewModel.searchField += "1"
}
@ -125,13 +121,9 @@ struct DialerBottomSheet: View {
Button {
if currentCall != nil {
do {
let digit = ("2".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "2"
} catch {
}
let digit = ("2".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "2"
} else {
startCallViewModel.searchField += "2"
}
@ -150,13 +142,9 @@ struct DialerBottomSheet: View {
Button {
if currentCall != nil {
do {
let digit = ("3".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "3"
} catch {
}
let digit = ("3".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "3"
} else {
startCallViewModel.searchField += "3"
}
@ -177,13 +165,9 @@ struct DialerBottomSheet: View {
HStack {
Button {
if currentCall != nil {
do {
let digit = ("4".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "4"
} catch {
}
let digit = ("4".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "4"
} else {
startCallViewModel.searchField += "4"
}
@ -202,13 +186,9 @@ struct DialerBottomSheet: View {
Button {
if currentCall != nil {
do {
let digit = ("5".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "5"
} catch {
}
let digit = ("5".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "5"
} else {
startCallViewModel.searchField += "5"
}
@ -227,13 +207,9 @@ struct DialerBottomSheet: View {
Button {
if currentCall != nil {
do {
let digit = ("6".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "6"
} catch {
}
let digit = ("6".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "6"
} else {
startCallViewModel.searchField += "6"
}
@ -255,13 +231,9 @@ struct DialerBottomSheet: View {
HStack {
Button {
if currentCall != nil {
do {
let digit = ("7".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "7"
} catch {
}
let digit = ("7".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "7"
} else {
startCallViewModel.searchField += "7"
}
@ -280,13 +252,9 @@ struct DialerBottomSheet: View {
Button {
if currentCall != nil {
do {
let digit = ("8".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "8"
} catch {
}
let digit = ("8".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "8"
} else {
startCallViewModel.searchField += "8"
}
@ -305,13 +273,9 @@ struct DialerBottomSheet: View {
Button {
if currentCall != nil {
do {
let digit = ("9".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "9"
} catch {
}
let digit = ("9".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "9"
} else {
startCallViewModel.searchField += "9"
}
@ -333,13 +297,9 @@ struct DialerBottomSheet: View {
HStack {
Button {
if currentCall != nil {
do {
let digit = ("*".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "*"
} catch {
}
let digit = ("*".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "*"
} else {
startCallViewModel.searchField += "*"
}
@ -393,13 +353,9 @@ struct DialerBottomSheet: View {
)
} else {
Button {
do {
let digit = ("0".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "0"
} catch {
}
let digit = ("0".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "0"
} label: {
Text("0")
.foregroundStyle(currentCall != nil ? .white : Color.grayMain2c600)
@ -416,13 +372,9 @@ struct DialerBottomSheet: View {
Button {
if currentCall != nil {
do {
let digit = ("#".cString(using: String.Encoding.utf8)?[0])!
try currentCall!.sendDtmf(dtmf: digit)
dialerField += "#"
} catch {
}
let digit = ("#".cString(using: String.Encoding.utf8)?[0])!
self.sendDtmf(dtmf: digit)
dialerField += "#"
} else {
startCallViewModel.searchField += "#"
}
@ -534,6 +486,21 @@ struct DialerBottomSheet: View {
orientation = newOrientation
}
}
func sendDtmf(dtmf: CChar) {
CoreContext.shared.doOnCoreQueue { core in
guard let call = self.currentCall, call.state == .StreamsRunning else {
Log.warn("Cannot send DTMF: call not active")
return
}
do {
try call.sendDtmf(dtmf: dtmf)
} catch {
Log.error("Cannot send DTMF \(dtmf) to call \(call.callLog?.callId ?? ""): \(error)")
}
}
}
}
#Preview {

View file

@ -168,21 +168,21 @@ struct AddParticipantsFragment: View {
.padding(.horizontal)
ScrollView {
ForEach(0..<contactsManager.lastSearch.count, id: \.self) { index in
ForEach(0..<contactsManager.avatarListModel.count, id: \.self) { index in
HStack {
HStack {
if index == 0
|| contactsManager.lastSearch[index].friend?.name!.lowercased().folding(
|| contactsManager.avatarListModel[index].name.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first
!= contactsManager.lastSearch[index-1].friend?.name!.lowercased().folding(
!= contactsManager.avatarListModel[index-1].name.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first {
Text(
String(
(contactsManager.lastSearch[index].friend?.name!.uppercased().folding(
(contactsManager.avatarListModel[index].name.uppercased().folding(
options: .diacriticInsensitive,
locale: .current
).first)!))
@ -198,40 +198,28 @@ struct AddParticipantsFragment: View {
.padding(.trailing, 5)
}
if index < contactsManager.avatarListModel.count,
let friend = contactsManager.avatarListModel[index].friend,
let photo = friend.photo,
!photo.isEmpty {
Avatar(contactAvatarModel: contactsManager.avatarListModel[index], avatarSize: 50)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
}
Avatar(contactAvatarModel: contactsManager.avatarListModel[index], avatarSize: 50)
Text((contactsManager.lastSearch[index].friend?.name ?? "")!)
Text(contactsManager.avatarListModel[index].name)
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
if let searchAddress = contactsManager.lastSearch[index].friend?.address?.asStringUriOnly() {
if addParticipantsViewModel.participantsToAdd.contains(where: {
$0.address.asStringUriOnly() == searchAddress
}) {
Image("check")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25)
.padding(.horizontal)
}
if addParticipantsViewModel.participantsToAdd.contains(where: {
$0.address.asStringUriOnly() == contactsManager.avatarListModel[index].address
}) {
Image("check")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25)
.padding(.horizontal)
}
}
}
.background(.white)
.onTapGesture {
if let addr = contactsManager.lastSearch[index].address {
if let addr = try? Factory.Instance.createAddress(addr: contactsManager.avatarListModel[index].address) {
addParticipantsViewModel.selectParticipant(addr: addr)
}
}

View file

@ -170,7 +170,18 @@ class MeetingViewModel: ObservableObject {
chatRoomParams.backend = ChatRoom.Backend.FlexisipChat
chatRoomParams.encryptionEnabled = true
chatRoomParams.subject = "Meeting ics"
self.conferenceScheduler?.sendInvitations(chatRoomParams: chatRoomParams)
if self.conferenceScheduler == nil {
Log.info("\(MeetingViewModel.TAG) ConferenceScheduler is nil, resetting...")
self.resetConferenceSchedulerAndListeners(core: core)
}
guard let scheduler = self.conferenceScheduler else {
Log.error("\(MeetingViewModel.TAG) ConferenceScheduler still nil after reset")
return
}
scheduler.sendInvitations(chatRoomParams: chatRoomParams)
} else {
Log.error("\(MeetingViewModel.TAG) Failed to create default chatroom parameters. This should not happen")
}
@ -178,8 +189,12 @@ class MeetingViewModel: ObservableObject {
private func resetConferenceSchedulerAndListeners(core: Core) {
self.mSchedulerDelegate = nil
self.conferenceScheduler = try? core.createConferenceScheduler()
self.conferenceScheduler = LinphoneUtils.createConferenceScheduler(core: core)
guard let scheduler = self.conferenceScheduler else {
Log.info("\(MeetingViewModel.TAG) ConferenceScheduler is nil after reset, nothing to cancel")
return
}
self.mSchedulerDelegate = ConferenceSchedulerDelegateStub(onStateChanged: { (_: ConferenceScheduler, state: ConferenceScheduler.State) in
Log.info("\(MeetingViewModel.TAG) Conference state changed \(state)")
if state == ConferenceScheduler.State.Error {
@ -190,8 +205,10 @@ class MeetingViewModel: ObservableObject {
ToastViewModel.shared.displayToast = true
}
} else if state == ConferenceScheduler.State.Ready {
let conferenceAddress = self.conferenceScheduler?.info?.uri
Log.info("\(MeetingViewModel.TAG) Conference info created, address will be \(conferenceAddress?.asStringUriOnly() ?? "'nil'")")
if let confInfo = scheduler.info, let conferenceAddress = confInfo.uri {
Log.info("\(MeetingViewModel.TAG) Conference info created, address will be \(conferenceAddress.asStringUriOnly())")
}
DispatchQueue.main.async {
ToastViewModel.shared.toastMessage = "Success_meeting_info_created_toast"
ToastViewModel.shared.displayToast = true
@ -249,7 +266,7 @@ class MeetingViewModel: ObservableObject {
self.conferenceCreatedEvent = true
}
})
self.conferenceScheduler?.addDelegate(delegate: self.mSchedulerDelegate!)
scheduler.addDelegate(delegate: self.mSchedulerDelegate!)
}
func schedule() {
@ -263,41 +280,69 @@ class MeetingViewModel: ObservableObject {
}
guard CoreContext.shared.networkStatusIsConnected else {
DispatchQueue.main.async {
ToastViewModel.shared.toastMessage = "Unavailable_network"
ToastViewModel.shared.displayToast = true
}
return
DispatchQueue.main.async {
ToastViewModel.shared.toastMessage = "Unavailable_network"
ToastViewModel.shared.displayToast = true
}
return
}
operationInProgress = true
CoreContext.shared.doOnCoreQueue { core in
Log.info("\(MeetingViewModel.TAG) Scheduling \(self.isBroadcastSelected ? "broadcast" : "meeting")")
if let conferenceInfo = (SharedMainViewModel.shared.displayedMeeting != nil ? SharedMainViewModel.shared.displayedMeeting!.confInfo : try? Factory.Instance.createConferenceInfo()) {
let localAccount = core.defaultAccount
conferenceInfo.organizer = localAccount?.params?.identityAddress
// Allows to have a chat room within the conference
conferenceInfo.setCapability(streamType: StreamType.Text, enable: true)
// Enable end-to-end encryption if client supports it
if LinphoneUtils.isEndToEndEncryptedChatAvailable(core: core) {
Log.info("\(MeetingViewModel.TAG) Requesting EndToEnd security level for conference")
conferenceInfo.securityLevel = Conference.SecurityLevel.EndToEnd
} else {
Log.info("\(MeetingViewModel.TAG) Requesting PointToPoint security level for conference")
conferenceInfo.securityLevel = Conference.SecurityLevel.PointToPoint
}
self.fillConferenceInfo(confInfo: conferenceInfo)
self.resetConferenceSchedulerAndListeners(core: core)
self.conferenceScheduler?.account = localAccount
// Will trigger the conference creation automatically
self.conferenceScheduler?.info = conferenceInfo
let conferenceInfo: ConferenceInfo?
if let displayedMeeting = SharedMainViewModel.shared.displayedMeeting {
conferenceInfo = displayedMeeting.confInfo
} else {
conferenceInfo = try? Factory.Instance.createConferenceInfo()
}
guard let confInfo = conferenceInfo else {
Log.error("\(MeetingViewModel.TAG) Failed to create conference info")
return
}
guard let localAccount = core.defaultAccount else {
Log.error("\(MeetingViewModel.TAG) Default account is nil")
return
}
guard let organizer = localAccount.params?.identityAddress else {
Log.error("\(MeetingViewModel.TAG) Account params or identityAddress is nil")
return
}
confInfo.organizer = organizer
confInfo.setCapability(streamType: .Text, enable: true)
// Enable end-to-end encryption if client supports it
//if isEndToEndEncryptedChatAvailable(core: core) {
if false {
Log.info("\(MeetingViewModel.TAG) Requesting EndToEnd security level for conference")
confInfo.securityLevel = .EndToEnd
} else {
Log.info("\(MeetingViewModel.TAG) Requesting PointToPoint security level for conference")
confInfo.securityLevel = .PointToPoint
}
if self.conferenceScheduler == nil {
Log.info("\(MeetingViewModel.TAG) ConferenceScheduler is nil, resetting...")
self.resetConferenceSchedulerAndListeners(core: core)
}
guard let scheduler = self.conferenceScheduler else {
Log.error("\(MeetingViewModel.TAG) ConferenceScheduler still nil after reset")
return
}
self.fillConferenceInfo(confInfo: confInfo)
scheduler.account = localAccount
scheduler.info = confInfo
}
}
// Warning: must be called from core queue. Removed the dispatchQueue.main.async in order to have the animation properly trigger.
func loadExistingMeeting(meeting: MeetingModel) {
@ -352,7 +397,13 @@ class MeetingViewModel: ObservableObject {
func cancelMeetingWithNotifications(meeting: MeetingModel) {
CoreContext.shared.doOnCoreQueue { core in
self.resetConferenceSchedulerAndListeners(core: core)
self.conferenceScheduler?.cancelConference(conferenceInfo: meeting.confInfo)
guard let scheduler = self.conferenceScheduler else {
Log.info("\(MeetingViewModel.TAG) ConferenceScheduler is nil after reset, nothing to cancel")
return
}
scheduler.cancelConference(conferenceInfo: meeting.confInfo)
}
}

View file

@ -133,17 +133,16 @@ class MeetingsListViewModel: ObservableObject {
}
if let index = self.meetingsList.firstIndex(where: { $0.model?.address == meetingToDelete.address }) {
if self.todayIdx > index {
// bump todayIdx one place up
self.todayIdx -= 1
}
self.meetingsList.remove(at: index)
if self.meetingsList.count == 1 && self.meetingsList[0].model == nil {
// Only remaining meeting is the fake TodayMeeting, remove it too
meetingsList.removeAll()
}
DispatchQueue.main.async {
if self.todayIdx > index {
// bump todayIdx one place up
self.todayIdx -= 1
}
self.meetingsList.remove(at: index)
if self.meetingsList.count == 1 && self.meetingsList[0].model == nil {
// Only remaining meeting is the fake TodayMeeting, remove it too
self.meetingsList.removeAll()
}
ToastViewModel.shared.toastMessage = "Success_toast_meeting_deleted"
ToastViewModel.shared.displayToast = true
}

View file

@ -77,6 +77,8 @@ struct SettingsFragment: View {
ScrollView {
VStack(spacing: 0) {
// TODO: Wait for VFS fix
/*
HStack(alignment: .center) {
Text("settings_security_title")
.default_text_style_800(styleSize: 18)
@ -134,6 +136,7 @@ struct SettingsFragment: View {
.zIndex(-1)
.transition(.move(edge: .top))
}
*/
HStack(alignment: .center) {
Text("settings_calls_title")

View file

@ -24,9 +24,8 @@ class AccountProfileViewModel: ObservableObject {
static let TAG = "[AccountProfileViewModel]"
@Published var dialPlanValueSelected: String = "🇫🇷 France | +33"
@Published var dialPlanValueSelected: String = ""
var dialPlanSelected: DialPlan?
var dialPlansList: [DialPlan] = []
@Published var accountModelIndex: Int? = 0
@Published var defaultAccountModelIndex: Int? = 0
@ -52,13 +51,11 @@ class AccountProfileViewModel: ObservableObject {
if self.getImagePath().lastPathComponent.contains("-default") || self.getImagePath().lastPathComponent == "Documents" {
let usernameTmp = CoreContext.shared.accounts[self.accountModelIndex!].usernaneAvatar
DispatchQueue.main.async {
self.saveImage(
image: ContactsManager.shared.textToImage(
firstName: displayNameAccountModel.isEmpty ? usernameTmp : displayNameAccountModel, lastName: ""),
name: usernameTmp,
prefix: "-default")
}
self.saveImage(
image: ContactsManager.shared.textToImage(
firstName: displayNameAccountModel.isEmpty ? usernameTmp : displayNameAccountModel, lastName: ""),
name: usernameTmp,
prefix: "-default")
}
}
@ -67,6 +64,10 @@ class AccountProfileViewModel: ObservableObject {
newParams?.internationalPrefix = self.dialPlanSelected?.countryCallingCode
newParams?.internationalPrefixIsoCountryCode = self.dialPlanSelected?.isoCountryCode
newParams?.useInternationalPrefixForCallsAndChats = true
} else if newParams?.useInternationalPrefixForCallsAndChats == true {
newParams?.internationalPrefix = nil
newParams?.internationalPrefixIsoCountryCode = nil
newParams?.useInternationalPrefixForCallsAndChats = false
}
CoreContext.shared.accounts[self.accountModelIndex!].account.params = newParams
@ -86,16 +87,20 @@ class AccountProfileViewModel: ObservableObject {
var dialPlanValueSelectedTmp = ""
if !prefix.isEmpty || !isoCountryCode.isEmpty {
Log.info(
"\(AccountProfileViewModel.TAG) Account \(accountTmp.account.params?.identityAddress?.asStringUriOnly() ?? "") prefix is \(prefix) \(isoCountryCode)"
)
self.dialPlansList = Factory.Instance.dialPlans
if let dialPlan = self.dialPlansList.first(where: { $0.isoCountryCode == isoCountryCode }) ??
self.dialPlansList.first(where: { $0.countryCallingCode == prefix }) {
dialPlanValueSelectedTmp = "\(dialPlan.flag) \(dialPlan.country) | +\(dialPlan.countryCallingCode)"
}
}
Log.info(
"\(AccountProfileViewModel.TAG) Account \(accountTmp.account.params?.identityAddress?.asStringUriOnly() ?? "") prefix is \(prefix) \(isoCountryCode)"
)
let dialPlansList = SharedMainViewModel.shared.dialPlansList
if let dialPlan = dialPlansList.first(where: { $0?.isoCountryCode == isoCountryCode }) ??
dialPlansList.first(where: { $0?.countryCallingCode == prefix }) {
dialPlanValueSelectedTmp = "\(dialPlan?.flag ?? "") \(dialPlan?.country ?? "") | +\(dialPlan?.countryCallingCode ?? "")"
} else {
dialPlanValueSelectedTmp = "No country code"
}
} else {
dialPlanValueSelectedTmp = "No country code"
}
let accountDisplayName = accountTmp.account.displayName()
@ -118,8 +123,9 @@ class AccountProfileViewModel: ObservableObject {
}
func updateDialPlan(newDialPlan: String) {
if let dialPlan = self.dialPlansList.first(where: { newDialPlan.contains($0.isoCountryCode) }) ??
self.dialPlansList.first(where: { newDialPlan.contains($0.countryCallingCode) }) {
let dialPlansList = SharedMainViewModel.shared.dialPlansList
if let dialPlan = dialPlansList.first(where: { newDialPlan.contains($0?.isoCountryCode ?? "") }) ??
dialPlansList.first(where: { newDialPlan.contains($0?.countryCallingCode ?? "") }) {
self.dialPlanSelected = dialPlan
}
}
@ -131,12 +137,14 @@ class AccountProfileViewModel: ObservableObject {
let photoAvatarModelKey = CoreContext.shared.accounts[self.accountModelIndex!].usernaneAvatar
ContactsManager.shared.awaitDataWrite(data: data, name: name, prefix: prefix) { _, result in
ContactsManager.shared.awaitDataWrite(data: data, name: name, prefix: prefix) { result in
UserDefaults.standard.set(result, forKey: photoAvatarModelKey)
CoreContext.shared.accounts[self.accountModelIndex ?? 0].photoAvatarModel = ""
CoreContext.shared.accounts[self.accountModelIndex ?? 0].imagePathAvatar = nil
NotificationCenter.default.post(name: NSNotification.Name("ImageChanged"), object: nil)
DispatchQueue.main.async {
CoreContext.shared.accounts[self.accountModelIndex ?? 0].photoAvatarModel = ""
CoreContext.shared.accounts[self.accountModelIndex ?? 0].imagePathAvatar = nil
NotificationCenter.default.post(name: NSNotification.Name("ImageChanged"), object: nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
CoreContext.shared.accounts[self.accountModelIndex ?? 0].photoAvatarModel = result

View file

@ -215,6 +215,9 @@ class AccountSettingsViewModel: ObservableObject {
newParams.limeServerUrl = self.limeServerUrl
self.accountModel.account.params = newParams
SharedMainViewModel.shared.updateDisableMeetingFeature()
print("\(AccountSettingsViewModel.TAG) Changes have been saved")
}
}

View file

@ -93,8 +93,6 @@ class AccountModel: ObservableObject {
let displayName = account.displayName()
let address = account.params?.identityAddress?.asString()
self.requestDevicesList()
let displayNameTmp = account.params?.identityAddress?.displayName ?? displayName
let usernaneAvatarTmp = account.contactAddress?.username ?? displayName
var photoAvatarModelTmp = ""
@ -105,13 +103,11 @@ class AccountModel: ObservableObject {
if !photoAvatarModelKey.isEmpty {
if preferences.object(forKey: photoAvatarModelKey) == nil {
DispatchQueue.main.async {
self.saveImage(
image: ContactsManager.shared.textToImage(
firstName: usernaneAvatarTmp, lastName: ""),
name: usernaneAvatarTmp,
prefix: "-default")
}
self.saveImage(
image: ContactsManager.shared.textToImage(
firstName: usernaneAvatarTmp, lastName: ""),
name: usernaneAvatarTmp,
prefix: "-default")
} else {
photoAvatarModelTmp = preferences.string(forKey: photoAvatarModelKey)!
}
@ -156,11 +152,14 @@ class AccountModel: ObservableObject {
}
private func computeNotificationsCount() {
let count = account.unreadChatMessageCount + account.missedCallsCount
SharedMainViewModel.shared.updateMissedCallsCount()
SharedMainViewModel.shared.updateUnreadMessagesCount()
DispatchQueue.main.async { [self] in
notificationsCount = count
CoreContext.shared.doOnCoreQueue { core in
let count = self.account.unreadChatMessageCount + self.account.missedCallsCount
SharedMainViewModel.shared.updateMissedCallsCount()
SharedMainViewModel.shared.updateUnreadMessagesCount()
DispatchQueue.main.async {
self.notificationsCount = count
}
}
}
@ -271,7 +270,7 @@ class AccountModel: ObservableObject {
let photoAvatarModelKey = name
ContactsManager.shared.awaitDataWrite(data: data, name: name, prefix: prefix) { _, result in
ContactsManager.shared.awaitDataWrite(data: data, name: name, prefix: prefix) { result in
UserDefaults.standard.set(result, forKey: photoAvatarModelKey)
self.photoAvatarModel = ""

View file

@ -35,7 +35,7 @@ class SharedMainViewModel: ObservableObject {
@Published var displayedConversation: ConversationModel?
@Published var displayedMeeting: MeetingModel?
@Published var dialPlansList: [DialPlan] = []
@Published var dialPlansList: [DialPlan?] = []
@Published var dialPlansLabelList: [String] = []
@Published var dialPlansShortLabelList: [String] = []
@ -47,6 +47,7 @@ class SharedMainViewModel: ObservableObject {
@Published var missedCallsCount: Int = 0
@Published var disableChatFeature: Bool = false
@Published var disableMeetingFeature: Bool = false
let welcomeViewKey = "welcome_view"
let generalTermsKey = "general_terms"
@ -94,6 +95,7 @@ class SharedMainViewModel: ObservableObject {
updateMissedCallsCount()
updateUnreadMessagesCount()
updateDisableChatFeature()
updateDisableMeetingFeature()
}
func changeWelcomeView() {
@ -141,10 +143,14 @@ class SharedMainViewModel: ObservableObject {
func getDialPlansList() {
CoreContext.shared.doOnCoreQueue { _ in
let dialPlans = Factory.Instance.dialPlans
var dialPlansListTmp: [DialPlan] = []
var dialPlansListTmp: [DialPlan?] = []
var dialPlansLabelListTmp: [String] = []
var dialPlansShortLabelListTmp: [String] = []
dialPlansListTmp.append(nil)
dialPlansLabelListTmp.append("No country code")
dialPlansShortLabelListTmp.append("---")
dialPlans.forEach { dialPlan in
dialPlansListTmp.append(dialPlan)
dialPlansLabelListTmp.append(
@ -224,4 +230,14 @@ class SharedMainViewModel: ObservableObject {
}
}
}
func updateDisableMeetingFeature() {
CoreContext.shared.doOnCoreQueue { core in
let disableMeetingFeatureTmp = CorePreferences.disableMeetings ||
!LinphoneUtils.isRemoteConferencingAvailable(core: core)
DispatchQueue.main.async {
self.disableMeetingFeature = disableMeetingFeatureTmp
}
}
}
}

View file

@ -26,38 +26,36 @@ struct EditContactView: UIViewControllerRepresentable {
class Coordinator: NSObject, CNContactViewControllerDelegate, UINavigationControllerDelegate {
func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
if let cnc = contact {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.parent.contact = cnc
let newContact = Contact(
identifier: cnc.identifier,
firstName: cnc.givenName,
lastName: cnc.familyName,
organizationName: cnc.organizationName,
jobTitle: "",
displayName: cnc.nickname,
sipAddresses: cnc.instantMessageAddresses.map { $0.value.service == "SIP" ? $0.value.username : "" },
phoneNumbers: cnc.phoneNumbers.map { PhoneNumber(numLabel: $0.label ?? "", num: $0.value.stringValue)},
imageData: ""
)
let imageThumbnail = UIImage(data: contact!.thumbnailImageData ?? Data())
ContactsManager.shared.saveImage(
image: imageThumbnail
?? ContactsManager.shared.textToImage(
firstName: cnc.givenName.isEmpty
&& cnc.familyName.isEmpty
&& cnc.phoneNumbers.first?.value.stringValue != nil
? cnc.phoneNumbers.first!.value.stringValue
: cnc.givenName, lastName: cnc.familyName),
name: cnc.givenName + cnc.familyName,
prefix: ((imageThumbnail == nil) ? "-default" : ""),
contact: newContact,
linphoneFriend: "Native address-book",
existingFriend: ContactsManager.shared.getFriendWithContact(contact: newContact)) {
MagicSearchSingleton.shared.searchForContacts()
}
}
self.parent.contact = cnc
let newContact = Contact(
identifier: cnc.identifier,
firstName: cnc.givenName,
lastName: cnc.familyName,
organizationName: cnc.organizationName,
jobTitle: "",
displayName: cnc.nickname,
sipAddresses: cnc.instantMessageAddresses.map { $0.value.service == "SIP" ? $0.value.username : "" },
phoneNumbers: cnc.phoneNumbers.map { PhoneNumber(numLabel: $0.label ?? "", num: $0.value.stringValue)},
imageData: ""
)
let imageThumbnail = UIImage(data: contact!.thumbnailImageData ?? Data())
ContactsManager.shared.saveImage(
image: imageThumbnail
?? ContactsManager.shared.textToImage(
firstName: cnc.givenName.isEmpty
&& cnc.familyName.isEmpty
&& cnc.phoneNumbers.first?.value.stringValue != nil
? cnc.phoneNumbers.first!.value.stringValue
: cnc.givenName, lastName: cnc.familyName),
name: cnc.givenName + cnc.familyName,
prefix: ((imageThumbnail == nil) ? "-default" : ""),
contact: newContact,
linphoneFriend: "Native address-book",
existingFriend: ContactsManager.shared.getFriendWithContact(contact: newContact)) {
MagicSearchSingleton.shared.searchForContacts()
}
}
viewController.dismiss(animated: true, completion: {})
}

View file

@ -0,0 +1,24 @@
import Foundation
import UIKit
import Combine
final class KeyboardResponder: ObservableObject {
@Published var currentHeight: CGFloat = 0
private var cancellables: Set<AnyCancellable> = []
init() {
let willShow = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.map { notification -> CGFloat in
(notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
}
let willHide = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
Publishers.Merge(willShow, willHide)
.receive(on: RunLoop.main)
.assign(to: \.currentHeight, on: self)
.store(in: &cancellables)
}
}

View file

@ -72,6 +72,32 @@ class LinphoneUtils: NSObject {
core.defaultAccount?.params?.conferenceFactoryUri != nil
}
public class func createConferenceScheduler(core: Core) -> ConferenceScheduler? {
let account = LinphoneUtils.getDefaultAccount()
if let url = account?.params?.ccmpServerUrl, !url.isEmpty {
Log.info(
"CCMP server URL has been set in Account's params, using CCMP conference scheduler"
)
let conferenceScheduler = try? core.createConferenceSchedulerWithType(
account: account,
schedulingType: .CCMP
)
return conferenceScheduler
}
Log.info(
"CCMP server URL hasn't been set in Account's params, using SIP conference scheduler"
)
let conferenceScheduler = try? core.createConferenceSchedulerWithType(
account: account,
schedulingType: .SIP
)
return conferenceScheduler
}
public class func createGroupCall(core: Core, account: Account?, subject: String) -> Conference? {
do {
let conferenceParams = try core.createConferenceParams(conference: nil)

View file

@ -94,11 +94,13 @@ final class MagicSearchSingleton: ObservableObject {
}
}
let sortedLastSearch = lastSearchFriend.sorted(by: {
$0.friend!.name!.lowercased().folding(options: .diacriticInsensitive, locale: .current)
<
$1.friend!.name!.lowercased().folding(options: .diacriticInsensitive, locale: .current)
})
let sortedLastSearch = lastSearchFriend.sorted {
let name1 = $0.friend?.name?.lowercased()
.folding(options: .diacriticInsensitive, locale: .current) ?? ""
let name2 = $1.friend?.name?.lowercased()
.folding(options: .diacriticInsensitive, locale: .current) ?? ""
return name1 < name2
}
var addedAvatarListModel: [ContactAvatarModel] = []
sortedLastSearch.forEach { searchResult in

View file

@ -108,6 +108,7 @@
D73449992BC6932A00778C56 /* MeetingWaitingRoomFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73449982BC6932A00778C56 /* MeetingWaitingRoomFragment.swift */; };
D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */; };
D737AEEF2DA011F2005C1280 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D737AEED2DA011F2005C1280 /* Localizable.strings */; };
D738ACEE2E857BF10039F7D1 /* KeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */; };
D7458F392E0BDCF4000C957A /* linphoneExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; };
D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; };
@ -306,6 +307,7 @@
D719ABCB2ABC769C00B41C10 /* AssistantView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantView.swift; sourceTree = "<group>"; };
D719ABCE2ABC779A00B41C10 /* AccountLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountLoginViewModel.swift; sourceTree = "<group>"; };
D71A0E182B485ADF0002C6CD /* ViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtension.swift; sourceTree = "<group>"; };
D71C266F2E819A0D001A7F92 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = Localizable/sk.lproj/Localizable.strings; sourceTree = "<group>"; };
D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListFragment.swift; sourceTree = "<group>"; };
D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactFragment.swift; sourceTree = "<group>"; };
D720E6AC2BAD822000DDFD87 /* ParticipantModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantModel.swift; sourceTree = "<group>"; };
@ -333,6 +335,7 @@
D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingWaitingRoomViewModel.swift; sourceTree = "<group>"; };
D737AEEE2DA011F2005C1280 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable/en.lproj/Localizable.strings; sourceTree = "<group>"; };
D737AEF02DA01203005C1280 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable/fr.lproj/Localizable.strings; sourceTree = "<group>"; };
D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardResponder.swift; sourceTree = "<group>"; };
D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = linphoneExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = "<group>"; };
D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = "<group>"; };
@ -582,6 +585,7 @@
D717071C2AC591EF0037746F /* Utils */ = {
isa = PBXGroup;
children = (
D738ACED2E857BEF0039F7D1 /* KeyboardResponder.swift */,
D7DF8BE82E2104E5003A3BC7 /* EmojiPickerView.swift */,
D703F7072DC8C5FF005B8F75 /* FilePicker.swift */,
D717A10D2CEB770D00849D92 /* ShareSheetController.swift */,
@ -1153,6 +1157,7 @@
de,
ru,
"zh-Hans",
sk,
);
mainGroup = D719ABAA2ABC67BF00B41C10;
packageReferences = (
@ -1277,6 +1282,7 @@
D7DC096F2CFA1D7600A6D47C /* AccountProfileFragment.swift in Sources */,
D717A10E2CEB772300849D92 /* ShareSheetController.swift in Sources */,
66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */,
D738ACEE2E857BF10039F7D1 /* KeyboardResponder.swift in Sources */,
D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */,
C6DC4E3D2C199C4E009096FD /* BundleExtenion.swift in Sources */,
D7343FEF2D3FE16C0059D784 /* HelpViewModel.swift in Sources */,
@ -1445,6 +1451,7 @@
D7F372C22E65C5190008B863 /* de */,
D7F372C32E65C51B0008B863 /* ru */,
D7F372C42E65C51E0008B863 /* zh-Hans */,
D71C266F2E819A0D001A7F92 /* sk */,
);
name = Localizable.strings;
sourceTree = "<group>";
@ -1460,7 +1467,7 @@
CODE_SIGN_ENTITLEMENTS = msgNotificationService/msgNotificationService.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
@ -1480,7 +1487,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.0;
MARKETING_VERSION = 6.0.2;
OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS";
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.msgNotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1503,7 +1510,7 @@
CODE_SIGN_ENTITLEMENTS = msgNotificationService/msgNotificationService.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
@ -1522,7 +1529,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.0;
MARKETING_VERSION = 6.0.2;
OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS";
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.msgNotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1662,7 +1669,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Linphone/Linphone.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"Linphone/Preview Content\"";
@ -1701,7 +1708,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.3;
MARKETING_VERSION = 6.0.0;
MARKETING_VERSION = 6.0.2;
OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS";
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1722,7 +1729,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Linphone/Linphone.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Linphone/Preview Content\"";
DEVELOPMENT_TEAM = Z2V957B3D6;
@ -1759,7 +1766,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.3;
MARKETING_VERSION = 6.0.0;
MARKETING_VERSION = 6.0.2;
OTHER_SWIFT_FLAGS = "$(inherited) -DUSE_CRASHLYTICS";
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1778,7 +1785,7 @@
CODE_SIGN_ENTITLEMENTS = linphoneExtension/linphoneExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@ -1793,7 +1800,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.0;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.linphoneExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1812,7 +1819,7 @@
CODE_SIGN_ENTITLEMENTS = linphoneExtension/linphoneExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Z2V957B3D6;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@ -1827,7 +1834,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.0;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.linphoneExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;

View file

@ -124,7 +124,7 @@
"location" : "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git",
"state" : {
"branch" : "stable",
"revision" : "a4f6e33f8b44044e642d7a6c220af07cbc2b4e4f"
"revision" : "4d51a91278236d1a22d880b769397c94a2bb7b3e"
}
},
{
@ -150,8 +150,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "e3f69fd321d0c9fcdc16fb576a0cdd956675face",
"version" : "1.31.0"
"revision" : "2547102afd04fe49f1b286090f13ebce07284980",
"version" : "1.31.1"
}
}
],

View file

@ -23,7 +23,7 @@ various ways:
## Help on translations
We no longer use transifex for the translation process, instead we have deployed our own instance of [Weblate](https://weblate.linphone.org/projects/linphone-iphone/).
We no longer use transifex for the translation process, instead we have deployed our own instance of [Weblate](https://weblate.linphone.org/projects/linphone/).
Due to the full app rewrite we can't re-use previous translations, so we'll be very happy if you want to contribute.