Compare commits

...

118 commits

Author SHA1 Message Date
Benoit Martins
c5c970d177 Fix logging in app extension 2026-01-15 17:45:32 +01:00
Benoit Martins
986276c04f Updated CHANGELOG & bumped version code to 6.1.0 (5) 2026-01-15 09:30:48 +01:00
Benoit Martins
221f0b10d7 Refresh view when config is applied 2026-01-12 16:32:42 +01:00
Benoit Martins
aed6876065 Remove videoCaptureEnabled and videoDisplayEnabled from CoreContext init 2026-01-09 16:47:56 +01:00
Benoit Martins
4f7e4b0c36 Fix CallView when the friend has only a phone number 2026-01-09 14:12:03 +01:00
Benoit Martins
6575a4b0f2 Updated CHANGELOG & bumped version code to 6.1.0 (4) 2026-01-06 16:29:35 +01:00
Benoit Martins
9723c0de04 Allow mentioning participants in conversation 2026-01-06 12:22:22 +01:00
Benoit Martins
990d2f36af Add mentions to DynamicLinkText in ChatBubbleView 2026-01-05 12:26:58 +01:00
Benoit Martins
8d5c0ce79b Add intents extension 2025-12-29 17:09:50 +01:00
Benoit Martins
61931138b2 Remove SDK message queued workaround 2025-12-23 16:48:26 +01:00
Benoit Martins
1dbbe6a53d Add support for MWI (voicemail) push notifications 2025-12-18 16:57:35 +01:00
Benoit Martins
4c45a9bb1e Disable video calls when core.videoEnabled is false 2025-12-18 10:42:28 +01:00
Benoit Martins
4feae7fccd Temporarily disable queued and pendingDelivery message handling awaiting SDK fix 2025-12-18 09:54:30 +01:00
Benoit Martins
1501dae019 Update SPM dependencies 2025-12-17 16:55:58 +01:00
Benoit Martins
84ec09173c Display message when a ChatMessage is queued 2025-12-17 16:20:36 +01:00
Benoit Martins
4cd63b53b2 Allow add-contact UI when disableAddContact is disabled 2025-12-17 15:36:54 +01:00
Benoit Martins
d4b10d38ae Use CorePreferences contactsFilter for contact filtering 2025-12-17 11:48:51 +01:00
Benoit Martins
6827bdc1dc Update UI on config change 2025-12-16 15:57:54 +01:00
Benoit Martins
ed08190ff4 Reset CallKit UUID on invite resend 2025-12-15 13:21:12 +01:00
Benoit Martins
57b7b857bc Replace contactAddress with params.identityAddress for defaultAccount 2025-12-12 14:40:03 +00:00
Benoit Martins
f16a0f42ae Add Message Waiting Indication 2025-12-12 14:40:03 +00:00
Christophe Deschamps
c7ddf2d8d0 Search for message to update after updating eventLog Ids 2025-12-09 17:56:11 +01:00
Benoit Martins
7decb12a3f Updated CHANGELOG & bumped version code to 6.1.0 (3) 2025-12-09 10:52:33 +01:00
Benoit Martins
da1abd75ae Update Git commit, branch, and tag info 2025-12-08 13:46:22 +01:00
Benoit Martins
33b379285f Updated CHANGELOG & bumped version code to 6.1.0 (2) 2025-12-08 11:52:40 +01:00
Benoit Martins
b45a328ad4 Updated translations from Weblate 2025-12-08 11:29:15 +01:00
Benoit Martins
c9f2915ca0 Add white progress indicator dot during seeking 2025-12-05 15:59:04 +01:00
Benoit Martins
fe8432f128 Add a bottom sheet in RecordingsListFragment and display an empty state when the list is empty 2025-12-05 15:57:53 +01:00
Benoit Martins
36fa752ccf Add seeking support to the audio record player 2025-12-03 23:04:51 +01:00
Benoit Martins
0f8df65dff Merge remote-tracking branch 'refs/remotes/origin/master'
#Conflicts:
#	LinphoneApp.xcodeproj/project.pbxproj
2025-12-02 15:53:56 +01:00
Benoit Martins
221e3cbb4b Add recording player 2025-12-02 15:30:12 +01:00
Benoit Martins
2b64c26518 Add automatic Git commit, branch, and tag info for Help views 2025-12-01 15:12:03 +01:00
Benoit Martins
5492a3e3a9 Update the unread message counter in onMessageRetracted 2025-12-01 14:33:59 +01:00
Benoit Martins
773102e4bd Fix the French translation of message_content_deleted 2025-12-01 13:45:49 +01:00
Benoit Martins
b462657a77 Add recording list 2025-11-28 17:32:59 +01:00
Benoit Martins
5d13a2b49d Stop composing when the user stops typing 2025-11-26 10:44:19 +01:00
Benoit Martins
7bdb8fa92d Update last message text in conversation list 2025-11-26 09:34:40 +00:00
Benoit Martins
0daba4fe03 New message deletion feature 2025-11-26 09:34:40 +00:00
Benoit Martins
07dbf407b0 Update PopupView UI 2025-11-26 09:34:40 +00:00
Benoit Martins
7972fd7c1f Add message editing feature 2025-11-26 09:34:40 +00:00
Benoit Martins
fa1f8386b4 Refresh presence info in history detail 2025-11-20 17:49:14 +01:00
Benoit Martins
a421d90d0c Refresh displayed friend when the contacts list is updated 2025-11-20 10:37:25 +01:00
Benoit Martins
b904f71f79 Displaying core call logs instead of account call logs when the user has only one account 2025-11-18 10:52:41 +01:00
Benoit Martins
e748a001bf Fix the prefix handling in interpretUrl when calling a phone number and add this in Settings 2025-11-17 15:29:45 +01:00
Benoit Martins
4fbb43f38c Fix SIP contacts filter 2025-11-17 13:44:59 +01:00
Benoit Martins
bcee4439f5 Updated CHANGELOG & bumped version code to 6.1.0 (intermediate version) 2025-11-13 11:10:53 +01:00
Benoit Martins
b49445d50d Update translations from Weblate 2025-11-13 10:26:35 +01:00
Benoit Martins
a3d2c74592 Update SPM dependencies 2025-11-05 10:10:05 +01:00
Benoit Martins
4f6dc2c0c7 Update Launch Screen (Spash Screen) 2025-11-04 15:37:11 +01:00
Benoit Martins
08ab9a1078 Use searchChatRoomByIdentifier instead of searchChatRoom in the changeDisplayedChatRoom 2025-11-03 12:06:32 +01:00
Benoit Martins
db72bdf242 Disable video button in audio-only mode during a call 2025-11-03 09:29:19 +01:00
Benoit Martins
9413f6f5dc Update encryption when call state changes 2025-10-30 17:52:42 +01:00
Benoit Martins
7237a5f4a7 Disable mediaEncryptionMandatory when mediaEncryption is set to None 2025-10-30 15:20:18 +01:00
Benoit Martins
8c9784a21d Store magicSearch.allContacts to display the entire contact list when the app starts 2025-10-30 15:04:19 +01:00
Benoit Martins
bb4134ede0 Enable phone number calls in contact details 2025-10-30 10:59:46 +01:00
Benoit Martins
8cd322ff0e Move disable_chat_feature to the UI section 2025-10-29 16:38:04 +01:00
Benoit Martins
f8b7e3b319 Fix international prefix reset in settings 2025-10-29 16:05:57 +01:00
Benoit Martins
aefa334038 Prevent editing when contact is read-only (LDAP contacts) 2025-10-29 08:28:08 +00:00
Benoit Martins
9559701a5e Remove addLdap from LdapViewModel since core.createLdapWithParams already adds it to the list 2025-10-29 08:28:08 +00:00
Benoit Martins
5bb757d150 Edit CardDav friend 2025-10-29 08:28:08 +00:00
Benoit Martins
ac6b478eb1 Edit carddav friend 2025-10-29 08:28:08 +00:00
Benoit Martins
41f9db8199 Add Ldap and Cardav settings 2025-10-29 08:28:08 +00:00
Benoit Martins
4b3d99245f Update account params if necessary in CoreContext 2025-10-20 15:19:19 +02:00
Benoit Martins
aae7c290be Enable core push notifications by default 2025-10-09 12:05:00 +02:00
Christophe Deschamps
0a06644473 Dynamic update of colours upon theme change 2025-10-04 08:59:52 +02:00
Christophe Deschamps
b75db70d19 Added reactive bridge for swift UI in Colour Themes 2025-10-04 08:59:46 +02:00
Christophe Deschamps
9beadaadd9 Theme - Display an optional picture in Help/About view specified by an URL (config entry ui/theme_about_picture_url) 2025-10-02 13:14:45 +00:00
Christophe Deschamps
888b75a2d4 Theme - Colors 2025-10-02 13:14:45 +00:00
Benoit Martins
068b93cf1c Hide SIP domains in suggestion list 2025-10-02 14:10:10 +02:00
Benoit Martins
e9eebbd45a Hide SIP addresses via settings 2025-10-01 15:08:37 +02:00
Benoit Martins
1c3680df65 Fix crash on defaultDomain with invalid UTF-8 strings 2025-10-01 09:25:09 +02:00
Benoit Martins
1389565b83 Remove auth info when logging out of an account 2025-09-30 17:53:19 +02:00
Benoit Martins
4fde1933dd Updated CHANGELOG & version code from release/6.0 branch 2025-09-26 16:27:27 +02:00
Benoit Martins
a33a8666ae Fix EditContactFragment view and allow '+' in number dialer 2025-09-26 16:02:55 +02:00
Benoit Martins
3b2ce2ed4b Fix dial plan selector and dial plan default 2025-09-26 16:02:55 +02:00
Benoit Martins
4cf1dbd8b5 Add advanced settings to third-party SIP account login view 2025-09-25 16:21:05 +02:00
Benoit Martins
1d0df11c61 Update translations from Weblate 2025-09-22 16:58:58 +02:00
Benoit Martins
ee1c09e98f Fix threading issues in saveImage and saveFriend in ContactsManager 2025-09-22 15:17:33 +02:00
Benoit Martins
8bd84ca8a5 New Fix crash when editing a contact by safely unwrapping friend/photo 2025-09-22 11:55:12 +02:00
Benoit Martins
1e53619eaa Revert "Fix crash when editing a contact by safely unwrapping friend/photo"
This reverts commit 112029d0df.
2025-09-22 10:25:05 +02:00
Benoit Martins
112029d0df Fix crash when editing a contact by safely unwrapping friend/photo 2025-09-19 16:49:33 +02:00
Benoit Martins
8737bcb40d Change French translation of manage_account_settings 2025-09-19 16:25:08 +02:00
Benoit Martins
5837e5e85d Disable meetings view when audio/video conference factory address is missing 2025-09-19 16:09:12 +02:00
Benoit Martins
e81699052a Stop requesting device list in AccountModel initializer 2025-09-19 11:37:03 +02:00
Benoit Martins
147682a0e5 Disable push notifications when pushNotificationAllowed is false 2025-09-19 11:16:17 +02:00
Benoit Martins
0dfdb5551c Update config files 2025-09-19 11:16:17 +02:00
Benoit Martins
880967d2e3 Fix meeting scheduler 2025-09-16 16:32:56 +02:00
Benoit Martins
b36f220911 Update friend list subscriptions on Core queue 2025-09-15 15:52:59 +02:00
Benoit Martins
a2564c4cc0 Compute notifications count in core queue 2025-09-15 15:34:43 +02:00
Benoit Martins
d3984b4b1a Fix call video display (nativePreviewWindow and nativeVideoWindow) 2025-09-15 15:18:21 +02:00
Benoit Martins
91c6f7a311 Fix call video display 2025-09-15 14:27:03 +02:00
Benoit Martins
de9656d94d Fix issue with meeting scheduling 2025-09-15 13:35:50 +02:00
Benoit Martins
43d63ae081 Reset the displayed chat room also when the chat room is empty 2025-09-15 12:11:02 +02:00
Benoit Martins
c7f7606859 Fix crash on core.queue by safely reading Strings from config 2025-09-15 11:36:07 +02:00
Benoit Martins
54ca929f88 Ensure call termination is executed on the Core queue 2025-09-15 11:16:44 +02:00
Benoit Martins
c6291f61d6 Add a burger button to open the side menu 2025-09-15 11:10:03 +02:00
Benoit Martins
42997d6891 Change the layout icon in the conference call 2025-09-15 10:51:31 +02:00
Benoit Martins
014e7c575e Updated CHANGELOG & version code from release/6.0 branch 2025-09-12 14:12:39 +02:00
Benoit Martins
773696260b Update translations from Weblate 2025-09-12 14:08:47 +02:00
Benoit Martins
304f46ba02 Add Done button toolbar to number pads 2025-09-11 18:14:13 +02:00
Benoit Martins
e0229fde0f Fix avatar photo refresh 2025-09-11 17:59:58 +02:00
Benoit Martins
108df56148 Fix onEphemeralMessageTimerStarted callback 2025-09-11 17:50:30 +02:00
Benoit Martins
c7a9edf25f Fix crash in updateEncryption by safely handling optional currentCall 2025-09-11 16:53:02 +02:00
Benoit Martins
90f2ad7e58 Fix friend list refresh triggered by onPresenceReceived 2025-09-11 16:31:14 +02:00
Benoit Martins
1485e7a574 Fix sorted list in MagicSearch when friend is nil 2025-09-11 10:52:41 +02:00
Benoit Martins
8f131ad335 Fix crash when adding or removing SIP addresses and phone numbers in EditContactFragment 2025-09-05 16:51:21 +02:00
Benoit Martins
bc1c737973 Use saveImage on core queue 2025-09-05 15:49:24 +02:00
Benoit Martins
926107061a Update textToImage to generate image on the core queue 2025-09-05 15:25:19 +02:00
Benoit Martins
3aa9419c5d Fix awaitDataWrite execution on main queue 2025-09-05 15:19:44 +02:00
Benoit Martins
a5bef93587 Send DTMF on the core queue 2025-09-05 14:38:50 +02:00
Benoit Martins
525e705b59 Prevent crash by copying Friend addresses and phone numbers before removal 2025-09-05 14:19:29 +02:00
Benoit Martins
a7e2ebb600 Ensure core is On before stopping it on background entry 2025-09-04 16:40:34 +02:00
Benoit Martins
14635250f8 Use point_to_point string for encrypted calls in conference 2025-09-04 15:16:03 +02:00
Benoit Martins
357418287c Add help view to login page 2025-09-04 15:00:52 +02:00
Benoit Martins
3c56f09130 Hide VFS setting 2025-09-04 15:00:09 +02:00
Benoit Martins
5b6e2e1c0a Fix textToImage crash 2025-09-04 12:00:09 +02:00
Benoit Martins
53a135a4b6 Update CHANGELOG.md 2025-09-02 09:53:08 +00:00
Benoit Martins
5035c6a924 Update README.md 2025-09-02 08:32:02 +00:00
138 changed files with 9003 additions and 2146 deletions

View file

@ -10,51 +10,121 @@ 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.1.0] - 2025-12-08
### Added
- LDAP and CardDAV settings
- Advanced settings in third-party SIP account login view
- Phone number calls in contact details
- Recording player
- Recording list
- Automatic Git commit, branch, and tag info for Help views
- Message deletion feature
- Message editing feature
### Changed
- Launch Screen (Splash Screen)
- Updated translations from Weblate
- Updated SPM dependencies
- Disabled meetings view when audio/video conference factory address is missing
- Moved disable_chat_feature to UI section
- Updated configuration files
- Updated last message text in conversation list
- Updated PopupView UI
- Display core call logs instead of account call logs when the user has only one account
### Fixed
- International prefix reset in settings
- Prevent editing of read-only (LDAP) contacts
- Crash when editing a contact (safe unwrapping of friend/photo)
- EditContactFragment view and “+” allowed in dialer
- Dial plan selector and default dial plan
- Encryption update when call state changes
- Unread message counter update in onMessageRetracted
- French translation of message_content_deleted
- Stop composing when the user stops typing
- Refresh presence info in history detail
- Refresh displayed friend when the contacts list is updated
- Prefix handling in interpretUrl when calling a phone number
- SIP contacts filter
## [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

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#4e6074" viewBox="0 0 256 256"><path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Zm-96-88v64a8,8,0,0,1-16,0V132.94l-4.42,2.22a8,8,0,0,1-7.16-14.32l16-8A8,8,0,0,1,112,120Zm59.16,30.45L152,176h16a8,8,0,0,1,0,16H136a8,8,0,0,1-6.4-12.8l28.78-38.37A8,8,0,1,0,145.07,132a8,8,0,1,1-13.85-8A24,24,0,0,1,176,136,23.76,23.76,0,0,1,171.16,150.45Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Zm-96-88v64a8,8,0,0,1-16,0V132.94l-4.42,2.22a8,8,0,0,1-7.16-14.32l16-8A8,8,0,0,1,112,120Zm59.16,30.45L152,176h16a8,8,0,0,1,0,16H136a8,8,0,0,1-6.4-12.8l28.78-38.37A8,8,0,1,0,145.07,132a8,8,0,1,1-13.85-8A24,24,0,0,1,176,136,23.76,23.76,0,0,1,171.16,150.45Z"></path></svg>

Before

Width:  |  Height:  |  Size: 604 B

After

Width:  |  Height:  |  Size: 604 B

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

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "music-notes.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" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="180" cy="164" r="28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="52" cy="196" r="28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="208" y1="72" x2="80" y2="104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="80 196 80 56 208 24 208 164" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>

After

Width:  |  Height:  |  Size: 664 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" fill="#4e6074" viewBox="0 0 256 256"><path d="M222.37,158.46l-47.11-21.11-.13-.06a16,16,0,0,0-15.17,1.4,8.12,8.12,0,0,0-.75.56L134.87,160c-15.42-7.49-31.34-23.29-38.83-38.51l20.78-24.71c.2-.25.39-.5.57-.77a16,16,0,0,0,1.32-15.06l0-.12L97.54,33.64a16,16,0,0,0-16.62-9.52A56.26,56.26,0,0,0,32,80c0,79.4,64.6,144,144,144a56.26,56.26,0,0,0,55.88-48.92A16,16,0,0,0,222.37,158.46ZM176,208A128.14,128.14,0,0,1,48,80,40.2,40.2,0,0,1,82.87,40a.61.61,0,0,0,0,.12l21,47L83.2,111.86a6.13,6.13,0,0,0-.57.77,16,16,0,0,0-1,15.7c9.06,18.53,27.73,37.06,46.46,46.11a16,16,0,0,0,15.75-1.14,8.44,8.44,0,0,0,.74-.56L168.89,152l47,21.05h0s.08,0,.11,0A40.21,40.21,0,0,1,176,208Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M222.37,158.46l-47.11-21.11-.13-.06a16,16,0,0,0-15.17,1.4,8.12,8.12,0,0,0-.75.56L134.87,160c-15.42-7.49-31.34-23.29-38.83-38.51l20.78-24.71c.2-.25.39-.5.57-.77a16,16,0,0,0,1.32-15.06l0-.12L97.54,33.64a16,16,0,0,0-16.62-9.52A56.26,56.26,0,0,0,32,80c0,79.4,64.6,144,144,144a56.26,56.26,0,0,0,55.88-48.92A16,16,0,0,0,222.37,158.46ZM176,208A128.14,128.14,0,0,1,48,80,40.2,40.2,0,0,1,82.87,40a.61.61,0,0,0,0,.12l21,47L83.2,111.86a6.13,6.13,0,0,0-.57.77,16,16,0,0,0-1,15.7c9.06,18.53,27.73,37.06,46.46,46.11a16,16,0,0,0,15.75-1.14,8.44,8.44,0,0,0,.74-.56L168.89,152l47,21.05h0s.08,0,.11,0A40.21,40.21,0,0,1,176,208Z"></path></svg>

Before

Width:  |  Height:  |  Size: 735 B

After

Width:  |  Height:  |  Size: 733 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#4e6074" viewBox="0 0 256 256"><path d="M176,160a39.89,39.89,0,0,0-28.62,12.09l-46.1-29.63a39.8,39.8,0,0,0,0-28.92l46.1-29.63a40,40,0,1,0-8.66-13.45l-46.1,29.63a40,40,0,1,0,0,55.82l46.1,29.63A40,40,0,1,0,176,160Zm0-128a24,24,0,1,1-24,24A24,24,0,0,1,176,32ZM64,152a24,24,0,1,1,24-24A24,24,0,0,1,64,152Zm112,72a24,24,0,1,1,24-24A24,24,0,0,1,176,224Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M176,160a39.89,39.89,0,0,0-28.62,12.09l-46.1-29.63a39.8,39.8,0,0,0,0-28.92l46.1-29.63a40,40,0,1,0-8.66-13.45l-46.1,29.63a40,40,0,1,0,0,55.82l46.1,29.63A40,40,0,1,0,176,160Zm0-128a24,24,0,1,1-24,24A24,24,0,0,1,176,32ZM64,152a24,24,0,1,1,24-24A24,24,0,0,1,64,152Zm112,72a24,24,0,1,1,24-24A24,24,0,0,1,176,224Z"></path></svg>

Before

Width:  |  Height:  |  Size: 431 B

After

Width:  |  Height:  |  Size: 431 B

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "trash.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,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>

After

Width:  |  Height:  |  Size: 415 B

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "voicemail.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="M200,72a56,56,0,0,0-39.14,96H95.14A56,56,0,1,0,56,184H200a56,56,0,0,0,0-112ZM16,128a40,40,0,1,1,40,40A40,40,0,0,1,16,128Zm184,40a40,40,0,1,1,40-40A40,40,0,0,1,200,168Z"></path></svg>

After

Width:  |  Height:  |  Size: 291 B

View file

@ -113,6 +113,8 @@ final class ContactsManager: ObservableObject {
core.addFriendList(list: tempRemoteFriendList)
}
}
self.refreshCardDavContacts()
}
let store = CNContactStore()
@ -154,25 +156,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 +196,165 @@ 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!)
func imageFromBase64(_ base64String: String) -> UIImage? {
let cleanedString: String
if let range = base64String.range(of: "base64,") {
cleanedString = String(base64String[range.upperBound...])
} else {
cleanedString = base64String
}
lblNameInitialize.text = textToDisplay.uppercased()
lblNameInitialize.textAlignment = .center
lblNameInitialize.backgroundColor = UIColor(Color.grayMain2c200)
lblNameInitialize.layer.cornerRadius = 10.0
guard let imageData = Data(base64Encoded: cleanedString, options: .ignoreUnknownCharacters) else {
print("Error: failed to decode Base64 string")
return nil
}
var IBImgViewUserProfile = UIImage()
UIGraphicsBeginImageContext(lblNameInitialize.frame.size)
lblNameInitialize.layer.render(in: UIGraphicsGetCurrentContext()!)
IBImgViewUserProfile = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return IBImgViewUserProfile
return UIImage(data: imageData)
}
func saveImage(image: UIImage, name: String, prefix: String, contact: Contact, linphoneFriend: String, existingFriend: Friend?, completion: @escaping () -> Void) {
func saveImage(image: UIImage, name: String, prefix: String, contact: Contact, linphoneFriend: String, existingFriend: Friend?, editingFriend: Bool = false, completion: @escaping () -> Void) {
guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else {
return
}
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!)
let base64Tmp = existingFriend?.friendList?.type == .CardDAV || linphoneAddressBookFriendList != CorePreferences.friendListInWhichStoreNewlyCreatedFriends
awaitDataWrite(data: data, name: name, prefix: prefix, base64: base64Tmp) { result in
if existingFriend?.friendList?.type != .CardDAV
|| (existingFriend?.friendList?.type == .CardDAV && linphoneFriend == self.linphoneAddressBookFriendList)
|| (editingFriend && linphoneFriend == CorePreferences.friendListInWhichStoreNewlyCreatedFriends) {
self.saveFriend(result: result, contact: contact, existingFriend: existingFriend) { resultFriend in
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 = core.friendsLists.first(where: { $0.type == .CardDAV && $0.displayName == CorePreferences.friendListInWhichStoreNewlyCreatedFriends }) {
if linphoneFL.type == .CardDAV {
_ = linphoneFL.addFriend(linphoneFriend: friend)
}
} else if let linphoneFL = self.tempRemoteFriendList {
if friend.friendList?.type != .CardDAV {
_ = linphoneFL.addFriend(linphoneFriend: friend)
}
}
} else if existingFriend == nil {
if let friendListTmp = self.friendList {
_ = friendListTmp.addLocalFriend(linphoneFriend: friend)
}
}
}
DispatchQueue.main.async {
completion()
}
}
}
completion()
} else {
DispatchQueue.main.async {
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: LinphoneUtils.applyInternationalPrefix(core: core)),
!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 = (friend.friendList?.type != .CardDAV && self.linphoneAddressBookFriendList == CorePreferences.friendListInWhichStoreNewlyCreatedFriends ? "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,25 +364,40 @@ 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
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((), "")
func awaitDataWrite(data: Data, name: String, prefix: String, base64: Bool? = false, completion: @escaping (String) -> Void) {
guard let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
completion("")
return
}
if base64 == false {
do {
let fileName = name + prefix + ".png"
let fileURL = directory.appendingPathComponent(fileName.replacingOccurrences(of: " ", with: ""))
try data.write(to: fileURL)
completion(fileName.replacingOccurrences(of: " ", with: ""))
} catch {
print("Error writing image: \(error)")
completion("")
}
} else {
do {
let fileName = name + prefix + ".png"
let fileURL = directory.appendingPathComponent(fileName.replacingOccurrences(of: " ", with: ""))
try data.write(to: fileURL)
if prefix.isEmpty {
let base64 = data.base64EncodedString()
completion("data:image/jpeg;base64,\(base64)")
} else {
completion("")
}
} catch {
print("Error writing image: \(error)")
completion("")
}
}
}
@ -399,15 +424,22 @@ final class ContactsManager: ObservableObject {
var friend: Friend?
if let friendList = self.friendList {
friend = friendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) })
friend = friendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) || $0.phoneNumbers.contains(where: { $0 == address.username }) })
}
if friend == nil, let linphoneFriendList = self.linphoneFriendList {
friend = linphoneFriendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) })
friend = linphoneFriendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) || $0.phoneNumbers.contains(where: { $0 == address.username }) })
}
if friend == nil, let tempRemoteFriendList = self.tempRemoteFriendList {
friend = tempRemoteFriendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) })
friend = tempRemoteFriendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) || $0.phoneNumbers.contains(where: { $0 == address.username }) })
}
CoreContext.shared.mCore.friendsLists.forEach { friendList in
if friendList.type == .CardDAV {
friend = friendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) || $0.phoneNumbers.contains(where: { $0 == address.username }) })
}
}
return friend
}
@ -425,6 +457,13 @@ final class ContactsManager: ObservableObject {
friendList.updateSubscriptions()
}
if let friendListDelegateToDelete = self.friendListDelegate {
CoreContext.shared.mCore.friendsLists.forEach { friendList in
friendList.removeDelegate(delegate: friendListDelegateToDelete)
}
}
self.friendListDelegate = nil
let friendListDelegateTmp = FriendListDelegateStub(
onContactCreated: { (friendList: FriendList, linphoneFriend: Friend) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactCreated")
@ -436,55 +475,142 @@ final class ContactsManager: ObservableObject {
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactUpdated")
},
onSyncStatusChanged: { (friendList: FriendList, status: FriendList.SyncStatus?, message: String?) in
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onSyncStatusChanged")
Log.info("\(ContactsManager.TAG) FriendListDelegateStub onSyncStatusChanged \(friendList.displayName ?? "No Display Name") -- Status: \(status != nil ? String(describing: status!) : "No Status")")
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)
}
}
}
}
let dispatchGroup = DispatchGroup()
friendList.friends.forEach { friend in
dispatchGroup.enter()
let addressTmp = friend.address?.clone()?.asStringUriOnly() ?? ""
let newContact = Contact(
identifier: UUID().uuidString,
firstName: friend.name ?? addressTmp,
lastName: "",
organizationName: "",
jobTitle: "",
firstName: friend.firstName ?? addressTmp,
lastName: friend.lastName ?? "",
organizationName: friend.organization ?? "",
jobTitle: friend.jobTitle ?? "",
displayName: friend.address?.displayName ?? "",
sipAddresses: friend.addresses.map { $0.asStringUriOnly() },
phoneNumbers: [],
phoneNumbers: friend.phoneNumbersWithLabel.map { PhoneNumber(numLabel: $0.label ?? "", num: $0.phoneNumber)},
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: UIImage?
if let photo = friend.photo, !photo.isEmpty, friendList.type == .CardDAV {
if let imageTmp = self.imageFromBase64(photo) {
image = imageTmp
if let image = image {
self.saveImage(
image: image,
name: friend.name ?? addressTmp,
prefix: "",
contact: newContact, linphoneFriend: friendList.displayName ?? "No Display Name", existingFriend: friend.friendList?.type == .CardDAV ? friend : nil) {
dispatchGroup.leave()
}
}
} else {
image = self.textToImage(firstName: friend.name ?? addressTmp, lastName: "")
if let image = image {
self.saveImage(
image: image,
name: friend.name ?? addressTmp,
prefix: "-default",
contact: newContact, linphoneFriend: friendList.displayName ?? "No Display Name", existingFriend: friend.friendList?.type == .CardDAV ? friend : nil) {
dispatchGroup.leave()
}
}
}
} else {
image = self.textToImage(firstName: friend.name ?? addressTmp, lastName: "")
if let image = image {
self.saveImage(
image: image,
name: friend.name ?? addressTmp,
prefix: "-default",
contact: newContact, linphoneFriend: friendList.displayName ?? "No Display Name", existingFriend: friend.friendList?.type == .CardDAV ? friend : 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 +648,8 @@ final class ContactsManager: ObservableObject {
}
)
self.friendListDelegate = friendListDelegateTmp
CoreContext.shared.mCore.friendsLists.forEach { friendList in
friendList.addDelegate(delegate: friendListDelegateTmp)
}
@ -530,6 +658,11 @@ final class ContactsManager: ObservableObject {
func addCoreDelegate(core: Core) {
self.coreContext.doOnCoreQueue { _ in
if let coreDelegate = self.coreDelegate {
core.removeDelegate(delegate: coreDelegate)
self.coreDelegate = nil
}
self.coreDelegate = CoreDelegateStub(
onFriendListCreated: { (_: Core, friendList: FriendList) in
Log.info("\(ContactsManager.TAG) Friend list \(friendList.displayName) created")
@ -558,15 +691,28 @@ 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()
}
}
}
func refreshCardDavContacts() {
self.coreContext.doOnCoreQueue { core in
core.friendsLists.forEach{ friendList in
if (friendList.type == .CardDAV) {
Log.info("\(ContactsManager.TAG) Found CardDAV friend list \(friendList.displayName), starting update")
friendList.synchronizeFriendsFromServer()
}
}
}
}
}

View file

@ -59,6 +59,8 @@ class CoreContext: ObservableObject {
var digestAuthInfoPendingPasswordUpdate: AuthInfo?
@Published var reloadID = UUID()
private init() {
do {
try initialiseCore()
@ -121,7 +123,7 @@ class CoreContext: ObservableObject {
coreQueue.async {
LoggingService.Instance.logLevel = LogLevel.Debug
Factory.Instance.logCollectionPath = Factory.Instance.getConfigDir(context: nil)
Factory.Instance.logCollectionPath = Factory.Instance.getDataDir(context: UnsafeMutablePointer<Int8>(mutating: (Config.appGroupName as NSString).utf8String))
Factory.Instance.enableLogCollection(state: LogCollectionState.Enabled)
Log.info("Checking if linphonerc file exists already. If not, creating one as a copy of linphonerc-default")
@ -148,13 +150,21 @@ class CoreContext: ObservableObject {
self.mCore.callkitEnabled = true
self.mCore.pushNotificationEnabled = true
let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
let appGitVersion = AppGitInfo.commit
let appGitBranch = AppGitInfo.branch
let appGitTag = AppGitInfo.tag
let sdkGitVersion = linphonesw.sdkVersion
var sdkGitBranch = linphonesw.sdkBranch
let userAgent = "LinphoneiOS/\(version ?? "6.0.0") (\(UIDevice.current.localizedModel.replacingOccurrences(of: "'", with: ""))) LinphoneSDK"
if sdkGitBranch.hasPrefix("remotes/origin/") {
sdkGitBranch = String(sdkGitBranch.dropFirst("remotes/origin/".count))
}
Log.info("Git Info — App: \(appGitTag + "-" + appGitVersion) [\(appGitBranch)] | SDK: \(sdkGitVersion) [\(sdkGitBranch)]")
let userAgent = "LinphoneiOS/\(appGitTag) (\(UIDevice.current.localizedModel.replacingOccurrences(of: "'", with: ""))) LinphoneSDK"
self.mCore.setUserAgent(name: userAgent, version: self.coreVersion)
self.mCore.videoCaptureEnabled = true
self.mCore.videoDisplayEnabled = true
self.mCore.videoPreviewEnabled = false
self.mCore.fecEnabled = true
@ -200,6 +210,26 @@ class CoreContext: ObservableObject {
self.forceRemotePushToMatchVoipPushSettings(account: acc)
}
let container = FileUtil.sharedContainerUrl()
let recordingsDir = container.appendingPathComponent("Library/Recordings")
let fm = FileManager.default
if !fm.fileExists(atPath: recordingsDir.path) {
do {
try fm.createDirectory(
at: recordingsDir,
withIntermediateDirectories: true,
attributes: nil
)
print("Recordings directory created.")
} catch {
print("Error creating directory: \(error)")
}
} else {
print("Recordings directory already exists.")
}
self.mCoreDelegate = CoreDelegateStub(onGlobalStateChanged: { (core: Core, state: GlobalState, _: String) in
if state == GlobalState.On {
#if DEBUG
@ -208,20 +238,14 @@ class CoreContext: ObservableObject {
let pushEnvironment = ""
#endif
for account in core.accountList {
let newParams = account.params?.clone()
if account.params?.pushNotificationConfig?.provider != ("apns" + pushEnvironment) {
let newParams = account.params?.clone()
Log.info("Account \(String(describing: newParams?.identityAddress?.asStringUriOnly())) - updating apple push provider from \(String(describing: newParams?.pushNotificationConfig?.provider)) to apns\(pushEnvironment)")
newParams?.pushNotificationConfig?.provider = "apns" + pushEnvironment
account.params = newParams
}
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
}
self.actionsToPerformOnCoreQueueWhenCoreIsStarted.forEach { $0(core) }
@ -305,6 +329,8 @@ class CoreContext: ObservableObject {
}
}, onConfiguringStatus: { (_: Core, status: ConfiguringState, message: String) in
Log.info("New configuration state is \(status) = \(message)\n")
let themeMainColor = CorePreferences.themeMainColor
SharedMainViewModel.shared.updateConfigChanges()
DispatchQueue.main.async {
if status == ConfiguringState.Successful {
var accountModels: [AccountModel] = []
@ -312,6 +338,8 @@ class CoreContext: ObservableObject {
accountModels.append(AccountModel(account: account, core: self.mCore))
}
self.accounts = accountModels
ThemeManager.shared.applyTheme(named: themeMainColor)
self.reloadID = UUID()
}
}
}, onLogCollectionUploadStateChanged: { (_: Core, _: Core.LogCollectionUploadState, info: String) in
@ -406,6 +434,31 @@ class CoreContext: ObservableObject {
DispatchQueue.main.async {
self.accounts = accountModels
}
}, onMessageWaitingIndicationChanged: { (core: Core, event: Event, mwi: MessageWaitingIndication) in
if (mwi.hasMessageWaiting()) {
let summaries = mwi.summaries
Log.info(
"[CoreContext][onMessageWaitingIndicationChanged] MWI NOTIFY received, messages are waiting (\(summaries.count) summaries)"
)
if let defaultAccount = core.defaultAccount?.params?.identityAddress, let mwiAccount = mwi.accountAddress, defaultAccount.weakEqual(address2: mwiAccount){
if !summaries.isEmpty {
let summary = summaries.first
DispatchQueue.main.async {
withAnimation {
SharedMainViewModel.shared.waitingMessageCount = Int(summary?.nbNew ?? 0)
}
}
}
}
} else {
Log.info("[CoreContext][onMessageWaitingIndicationChanged] MWI NOTIFY received, no message is waiting")
DispatchQueue.main.async {
withAnimation {
SharedMainViewModel.shared.waitingMessageCount = 0
}
}
}
})
self.mCore.addDelegate(delegate: self.mCoreDelegate)
@ -424,26 +477,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,13 +133,23 @@ 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)
}
}
static var disableAddContact: Bool {
get {
return Config.get().getBool(section: "ui", key: "disable_add_contact", defaultValue: false)
}
set {
Config.get().setBool(section: "ui", key: "disable_add_contact", value: newValue)
}
}
static var showFavoriteContacts: Bool {
get {
return Config.get().getBool(section: "ui", key: "show_favorites_contacts", defaultValue: true)
@ -147,6 +159,15 @@ class CorePreferences {
}
}
static var friendListInWhichStoreNewlyCreatedFriends: String {
get {
return Config.get().getString(section: "app", key: "friend_list_to_store_newly_created_contacts", defaultString: "Linphone address-book")
}
set {
Config.get().setString(section: "app", key: "friend_list_to_store_newly_created_contacts", value: newValue)
}
}
static var voiceRecordingMaxDuration: Int {
get {
return Config.get().getInt(section: "app", key: "voice_recording_max_duration", defaultValue: 600000)
@ -177,13 +198,20 @@ 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)
}
}
static var themeAboutPictureUrl: String? {
get {
return Config.get().getString(section: "ui", key: "theme_about_picture_url", defaultString: nil)
}
}
static var darkModeAllowed: Bool {
return Config.get().getBool(section: "ui", key: "dark_mode_allowed", defaultValue: true)
}
@ -244,21 +272,22 @@ 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)
}
}
static var disableChatFeature: Bool {
get {
return Config.get().getBool(section: "app", key: "disable_chat_feature", defaultValue: false)
}
set {
Config.get().setBool(section: "app", key: "disable_chat_feature", value: newValue)
}
}
static var disableChatFeature: Bool {
get {
return Config.get().getBool(section: "ui", key: "disable_chat_feature", defaultValue: false)
}
set {
Config.get().setBool(section: "ui", key: "disable_chat_feature", value: newValue)
}
}
static var disableMeetings: Bool {
get {
@ -269,6 +298,15 @@ class CorePreferences {
}
}
static var hideSipAddresses: Bool {
get {
return Config.get().getBool(section: "ui", key: "hide_sip_addresses", defaultValue: false)
}
set {
Config.get().setBool(section: "ui", key: "hide_sip_addresses", value: newValue)
}
}
private func copy(from: String, to: String, overrideIfExists: Bool = false) {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: to), !overrideIfExists {
@ -283,4 +321,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) {
return String(decoding: data, as: UTF8.self)
}
return defaultValue
}
}

View file

@ -0,0 +1,7 @@
import Foundation
public enum AppGitInfo {
public static let branch = "master"
public static let commit = "986276c04"
public static let tag = "6.1.0-alpha"
}

View file

@ -4,6 +4,16 @@
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>linphone-mention</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
@ -115,12 +125,6 @@
</array>
</dict>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<true/>
<key>ITSEncryptionExportComplianceCode</key>
<string>b5cb085f-772a-4a4f-8c77-5d1332b1f93f</string>
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
<string></string>
<key>NSSupportsSuddenTermination</key>
<false/>
<key>UIAppFonts</key>
@ -139,9 +143,6 @@
<string>audio</string>
</array>
<key>UILaunchScreen</key>
<dict>
<key>UIImageName</key>
<string>linphone</string>
</dict>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24128" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="6L3-wz-ibv" image="linphone">
<rect key="frame" x="0" y="0" width="240" height="128"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<!-- Center horizontally -->
<constraint firstItem="6L3-wz-ibv" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="centerXConstraint"/>
<!-- Center vertically -->
<constraint firstItem="6L3-wz-ibv" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="centerYConstraint"/>
<!-- Fixed width -->
<constraint firstItem="6L3-wz-ibv" firstAttribute="width" constant="240" id="widthConstraint"/>
<!-- Fixed height -->
<constraint firstItem="6L3-wz-ibv" firstAttribute="height" constant="128" id="heightConstraint"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="linphone" width="100" height="102"/>
</resources>
</document>

View file

@ -20,6 +20,7 @@
import SwiftUI
import linphonesw
import UserNotifications
import Intents
let accountTokenNotification = Notification.Name("AccountCreationTokenReceived")
var displayedChatroomPeerAddr: String?
@ -108,7 +109,28 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
}
}
}
}
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard let interaction = userActivity.interaction,
let intent = interaction.intent as? INStartCallIntent,
let person = intent.contacts?.first,
let number = person.personHandle?.value else { return false }
let isVideo = intent.callCapability == .videoCall
Log.info("[AppDelegate][INStartCallIntent] Generic call intent received for number: \(number) isVideo: \(isVideo)")
CoreContext.shared.performActionOnCoreQueueWhenCoreIsStarted { core in
if let address = core.interpretUrl(url: number, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) {
TelecomManager.shared.doCallOrJoinConf(address: address, isVideo: isVideo)
}
}
return true
}
func applicationWillTerminate(_ application: UIApplication) {
@ -240,6 +262,22 @@ struct RootView: View {
pendingURL = url
}
}
.onContinueUserActivity("INStartCallIntent") { activity in
guard let interaction = activity.interaction,
let intent = interaction.intent as? INStartCallIntent,
let person = intent.contacts?.first,
let number = person.personHandle?.value else { return }
let isVideo = intent.callCapability == .videoCall
Log.info("[INStartCallIntent] Generic call intent received for number: \(number) isVideo: \(isVideo)")
coreContext.performActionOnCoreQueueWhenCoreIsStarted { core in
if let address = core.interpretUrl(url: number, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) {
telecomManager.doCallOrJoinConf(address: address, isVideo: isVideo)
}
}
}
}
@ -259,6 +297,7 @@ struct MainViewSwitcher: View {
let sharedMainViewModel: SharedMainViewModel
@Binding var pendingURL: URL?
let appDelegate: AppDelegate
@ObservedObject private var colors = ColorProvider.shared
var body: some View {
selectedMainView()
@ -277,5 +316,6 @@ struct MainViewSwitcher: View {
navigationManager.openChatRoom(callId: callId, peerAddr: peerAddr, localAddr: localAddr)
}
}
.id(colors.theme.name)
}
}

View file

@ -0,0 +1 @@

View file

@ -2,7 +2,7 @@
"assistant_sip_account_transport_protocol" = "Transport";
"contact_call_action" = "Volat";
"conversation_action_call" = "Volat";
"dialog_ok" = "OK";
"dialog_confirm" = "Confirm";
"drawer_menu_manage_account" = "Spravovat profil";
"help_error_checking_version_toast_message" = "Během kontroly aktualizací nastala chyba";
"settings_contacts_carddav_name_title" = "Zobrazené jméno";
@ -474,9 +474,9 @@
"message_copied_to_clipboard_toast" = "Zpráva zkopírována do schránky";
"message_delivery_info_read_title" = "Přečteno";
"message_delivery_info_received_title" = "Přijato";
"message_meeting_invitation_cancelled_notification" = "📅 Schůzka byla zrušena";
"message_meeting_invitation_notification" = "📅 Jste pozváni na schůzku";
"message_meeting_invitation_updated_notification" = "📅 Schůzka byla aktualizována";
"message_meeting_invitation_cancelled_notification" = "Schůzka byla zrušena";
"message_meeting_invitation_notification" = "Jste pozváni na schůzku";
"message_meeting_invitation_updated_notification" = "Schůzka byla aktualizována";
"message_reactions_info_all_title" = "Reakce";
"network_reachable_again" = "Síť je znovu dostupná";
"menu_block_number" = "Blokovat číslo";
@ -522,3 +522,4 @@
"conversations_files_waiting_to_be_shared_single" = "1 soubor čekající na sdílení";
"conversations_files_waiting_to_be_shared_multiple" = "%@ souborů čekajících na sdílení";
"conversation_ephemeral_messages_duration_multiple_days" = "%d dnů";
"authentication_id" = "ID pro ověření (je-li odlišné)";

View file

@ -1,7 +1,7 @@
"call_stats_audio_title" = "Audio";
"Error" = "Fehler";
"settings_advanced_accept_early_media_title" = "'Early Media' erlauben";
"settings_advanced_allow_outgoing_early_media_title" = "Ausgehende 'Early Media' erlauben";
"settings_advanced_accept_early_media_title" = "Frühe Medienübertragung erlauben";
"settings_advanced_allow_outgoing_early_media_title" = "Ausgehende frühe Medianübertragung erlauben";
"settings_advanced_audio_codecs_title" = "Audio-Codecs";
"settings_advanced_audio_devices_title" = "Audiogeräte";
"settings_advanced_device_id" = "Gerät ID";
@ -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";
@ -67,10 +67,10 @@
"call_action_change_layout" = "Layout";
"call_action_go_to_calls_list" = "Anrufliste";
"call_action_hang_up" = "Auflegen";
"call_action_pause_call" = "Pause";
"call_action_pause_call" = "Halten";
"call_action_record_call" = "Aufnahmen";
"call_action_resume_call" = "Fortsetzen";
"call_action_show_dialer" = "Wähler";
"call_action_show_dialer" = "Tastatur";
"call_action_show_messages" = "Nachrichten";
"call_action_start_new_call" = "Neuer Anruf";
"call_audio_device_type_earpiece" = "Ohrhörer";
@ -142,7 +142,7 @@
"conversation_action_delete" = "Gespräch löschen";
"conversation_action_leave_group" = "Verlasse die Gruppe";
"conversation_action_mark_as_read" = "Als gelesen markieren";
"conversation_action_mute" = "Stumm";
"conversation_action_mute" = "Stummschalten";
"conversation_action_unmute" = "Stumm aufheben";
"conversation_add_participants_title" = "Teilnehmer hinzufügen";
"conversation_dialog_edit_subject" = "Gesprächsbetreff bearbeiten";
@ -156,7 +156,7 @@
"conversation_ephemeral_messages_duration_three_days" = "3 Tage";
"conversation_ephemeral_messages_subtitle" = "Neue Nachrichten werden automatisch gelöscht, sobald sie von allen gelesen wurden.\nWählen Sie eine Dauer:";
"conversation_ephemeral_messages_title" = "Kurzlebiger Nachrichten";
"conversation_event_conference_created" = "Sie haben der Gruppe beigetreten";
"conversation_event_conference_created" = "Sie sind der Gruppe beigetreten";
"conversation_event_conference_destroyed" = "Sie haben der Gruppe verlassen";
"conversation_event_ephemeral_messages_disabled" = "Kurzlebige Nachrichten wurden deaktiviert";
"conversation_event_ephemeral_messages_enabled" = "Kurzlebige Nachrichten wurden aktiviert";
@ -192,7 +192,7 @@
"dialog_deny" = "Ablehnen";
"dialog_install" = "Installieren";
"dialog_no" = "Nein";
"dialog_ok" = "OK";
"dialog_confirm" = "Confirm";
"dialog_yes" = "Ja";
"drawer_menu_account_connection_status_cleared" = "Deaktiviert";
"drawer_menu_account_connection_status_connected" = "Verbunden";
@ -288,17 +288,17 @@
"menu_delete_selected_item" = "Löschen";
"menu_forward_chat_message" = "Weiterleiten";
"menu_invite" = "Einladen";
"menu_reply_to_chat_message" = "Antwort";
"menu_reply_to_chat_message" = "Antworten";
"menu_resend_chat_message" = "Erneut senden";
"menu_see_existing_contact" = "Siehe Kontakt";
"menu_show_imdn" = "Lieferstatus";
"menu_show_imdn" = "Zustelldetails";
"message_delivery_info_error_title" = "Fehler";
"message_forwarded_label" = "Weitergeleitet";
"message_reaction_click_to_remove_label" = "Zum Entfernen klicken";
"network_not_reachable" = "Sie sind nicht mit dem Internet verbunden";
"new_conversation_create_group" = "Gruppengespräch erstellen";
"new_conversation_search_bar_filter_hint" = "Kontakt suchen";
"new_conversation_title" = "Neues Gespräch";
"new_conversation_title" = "Neuer Chat";
"next" = "Weiter";
"notification_missed_call_title" = "Verpasster Anruf";
"operation_in_progress_overlay" = "Vorgang wird ausgeführt, bitte warten";
@ -318,29 +318,29 @@
"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";
"settings_calls_title" = "Anrufe";
"settings_calls_vibrate_while_ringing_title" = "Vibrieren während ein eingehender Anruf klingelt";
"settings_contacts_add_carddav_server_title" = "CardDAV Adressbuch hinzufügen";
"settings_contacts_add_ldap_server_title" = "LDAP Server hinzufügen";
"settings_contacts_add_carddav_server_title" = "CardDAV-Adressbuch hinzufügen";
"settings_contacts_add_ldap_server_title" = "LDAP-Server hinzufügen";
"settings_contacts_carddav_name_title" = "Anzeigename";
"settings_contacts_carddav_password_title" = "Passwort";
"settings_contacts_carddav_server_url_title" = "Server URL";
"settings_contacts_carddav_server_url_title" = "Server-URL";
"settings_contacts_carddav_sync_error_toast" = "Synchronisierungfehler!";
"settings_contacts_carddav_username_title" = "Benutzername";
"settings_contacts_edit_carddav_server_title" = "CardDAV Adressbuch bearbeiten";
"settings_contacts_edit_ldap_server_title" = "LDAP Server bearbeiten";
"settings_contacts_edit_carddav_server_title" = "CardDAV-Adressbuch bearbeiten";
"settings_contacts_edit_ldap_server_title" = "LDAP-Server bearbeiten";
"settings_contacts_ldap_bind_dn_title" = "DN binden";
"settings_contacts_ldap_password_title" = "Passwort";
"settings_contacts_ldap_search_base_title" = "Filter";
"settings_contacts_ldap_server_url_title" = "Server URL (darf nicht leer sein)";
"settings_contacts_ldap_search_base_title" = "Suchbasis (darf nicht leer sein)";
"settings_contacts_ldap_server_url_title" = "Server-URL (darf nicht leer sein)";
"settings_contacts_ldap_use_tls_title" = "TLS verwenden";
"settings_contacts_title" = "Kontakte";
"settings_conversations_auto_download_title" = "Dateien automatisch heruntergeladene";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Gespräche beim Schließen der Nachrichtenbenachrichtigung als gelesen markieren";
"settings_conversations_auto_download_title" = "Dateien automatisch herunterladen";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Gespräche beim Schließen der Benachrichtigung als gelesen markieren";
"settings_conversations_title" = "Gespräche";
"settings_meetings_default_layout_title" = "Standardlayout";
"settings_meetings_layout_active_speaker_label" = "Aktiver Lautsprecher";
@ -358,8 +358,154 @@
"sip_address_display_name" = "Anzeigename";
"sip_address_domain" = "Domäne";
"start" = "Start";
"web_platform_register_email_url" = "TURN aktivieren";
"web_platform_register_email_url" = "https://subscribe.linphone.org/register/email";
"welcome_page_3_title" = "Open source";
"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" = "%@ schreiben…";
"conversation_composing_label_single" = "%@ schreibt…";
"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" = "Gruppenteilnehmer (%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" = "Empfangen";
"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" = "Authentifizierungsbereich";
"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" = "Filter";
"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";
"help_about_open_source_licenses_title" = "GNU General Public License v3.0";
"help_about_open_source_licenses_subtitle" = "© Belledonne Communications 2010-2024";

View file

@ -47,6 +47,8 @@
"account_settings_im_encryption_mandatory_title" = "IM encryption mandatory";
"account_settings_lime_server_url_title" = "E2E encryption keys server URL";
"account_settings_mwi_uri_title" = "MWI server URI (Message Waiting Indicator)";
"account_settings_apply_international_prefix_title" = "Format phone numbers using international prefix";
"account_settings_replace_plus_by_00_title" = "Replace + by 00 when formatting phone numbers";
"account_settings_nat_policy_title" = "NAT policy settings";
"account_settings_outbound_proxy_title" = "Outbound proxy";
"account_settings_push_notification_not_available_title" = "Push notifications aren't available!";
@ -93,6 +95,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";
@ -187,6 +190,7 @@
"contacts_list_favourites_title" = "Favourites";
"contacts_list_filter_popup_see_all" = "See all";
"contacts_list_filter_popup_see_linphone_only" = "See %@ contacts";
"contacts_list_filter_popup_see_sip_only" = "See SIP contacts";
"conversation_action_call" = "Call";
"conversation_action_configure_ephemeral_messages" = "Configure ephemeral messages";
"conversation_action_delete" = "Delete conversation";
@ -200,6 +204,13 @@
"conversation_dialog_edit_subject" = "Edit conversation subject";
"conversation_dialog_set_subject" = "Set conversation subject";
"conversation_dialog_subject_hint" = "Conversation subject";
"conversation_editing_message_title" = "Message being edited";
"conversation_message_edited_label" = "Edited";
"conversation_dialog_delete_chat_message_title" = "Delete this message?";
"conversation_dialog_delete_locally_label" = "For me";
"conversation_dialog_delete_for_everyone_label" = "For everyone";
"conversation_message_content_deleted_label" = "This message has been deleted";
"conversation_message_content_deleted_by_us_label" = "You have deleted this message";
"conversation_end_to_end_encrypted_bottom_sheet_link" = "https://linphone.org/en/features/#security";
"conversation_end_to_end_encrypted_event_title" = "End-to-end encrypted conversation";
"conversation_end_to_end_encrypted_event_subtitle" = "Messages in this conversation are e2e encrypted. Only your correspondent can decrypt them.";
@ -247,6 +258,8 @@
"conversation_message_meeting_cancelled_label" = "Meeting has been cancelled!";
"conversation_message_meeting_updated_label" = "Meeting has been updated";
"conversation_one_to_one_hidden_subject" = "Dummy subject";
"conversation_participants_list_empty" = "No participants found";
"conversation_participants_list_header" = "Participants";
"conversation_reply_to_message_title" = "Replying to: ";
"conversation_text_field_hint" = "Say something…";
"conversations_list_empty" = "No conversation for the moment…";
@ -266,7 +279,7 @@
"dialog_deny" = "Deny";
"dialog_install" = "Install";
"dialog_no" = "No";
"dialog_ok" = "OK";
"dialog_confirm" = "Confirm";
"dialog_yes" = "Yes";
"drawer_menu_account_connection_status_cleared" = "Disabled";
"drawer_menu_account_connection_status_connected" = "Connected";
@ -390,21 +403,26 @@
"menu_delete_selected_item" = "Delete";
"menu_forward_chat_message" = "Forward";
"menu_invite" = "Invite";
"menu_edit_chat_message" = "Edit";
"menu_reply_to_chat_message" = "Reply";
"menu_resend_chat_message" = "Re-send";
"menu_see_existing_contact" = "See contact";
"menu_show_imdn" = "Delivery status";
"menu_export_selected_item" = "Download";
"menu_share_selected_item" = "Share";
"message_copied_to_clipboard_toast" = "Message copied into clipboard";
"message_delivery_info_error_title" = "Error";
"message_delivery_info_read_title" = "Read";
"message_delivery_info_received_title" = "Received";
"message_delivery_info_sent_title" = "Sent";
"message_forwarded_label" = "Forwarded";
"message_meeting_invitation_cancelled_notification" = "📅 Meeting has been cancelled";
"message_meeting_invitation_notification" = "📅 You are invited to a meeting";
"message_meeting_invitation_updated_notification" = "📅 Meeting has been updated";
"message_meeting_invitation_cancelled_notification" = "Meeting has been cancelled";
"message_meeting_invitation_notification" = "You are invited to a meeting";
"message_meeting_invitation_updated_notification" = "Meeting has been updated";
"message_reaction_click_to_remove_label" = "Click to remove";
"message_reactions_info_all_title" = "Reactions";
"mwi_messages_are_waiting_single" = "1 new voice message";
"mwi_messages_are_waiting_multiple" = "%@ new voice messages";
"network_not_reachable" = "You aren't connected to internet";
"network_reachable_again" = "Network is now reachable again";
"new_conversation_create_group" = "Create a group conversation";
@ -423,6 +441,10 @@
"picker_categories" = "Categories";
"qr_code_validated" = "QR code validated";
"recordings_title" = "Recordings";
"recordings_list_empty" = "No recording for the moment…";
"recordings_list_empty" = "No recording for the moment…";
"recordings_list_empty" = "No recording for the moment…";
"recordings_list_empty" = "No recording for the moment…";
"selected_participants_count" = "%@ selected participants";
"settings_advanced_accept_early_media_title" = "Accept early media";
"settings_advanced_allow_outgoing_early_media_title" = "Allow outgoing early media";
@ -466,15 +488,22 @@
"settings_contacts_carddav_username_title" = "Username";
"settings_contacts_edit_carddav_server_title" = "Edit CardDAV address book";
"settings_contacts_edit_ldap_server_title" = "Edit LDAP server";
"settings_contacts_ldap_enabled_title" = "Enabled";
"settings_contacts_ldap_server_url_title" = "Server URL (can't be empty)";
"settings_contacts_ldap_bind_dn_title" = "Bind DN";
"settings_contacts_ldap_bind_user_password_title" = "Bind user password";
"settings_contacts_ldap_max_results_title" = "Maximum results";
"settings_contacts_ldap_password_title" = "Password";
"settings_contacts_ldap_request_timeout_title" = "Request timeout";
"settings_contacts_ldap_use_tls_title" = "Use TLS";
"settings_contacts_ldap_search_base_title" = "Search base (can't be empty)";
"settings_contacts_ldap_search_filter_title" = "Filter";
"settings_contacts_ldap_server_url_title" = "Server URL (can't be empty)";
"settings_contacts_ldap_use_tls_title" = "Use TLS";
"settings_contacts_ldap_max_results_title" = "Max results";
"settings_contacts_ldap_request_timeout_title" = "Timeout (in seconds)";
"settings_contacts_ldap_request_delay_title" = "Delay between two queries (in milliseconds)";
"settings_contacts_ldap_min_characters_title" = "Min characters to start a query";
"settings_contacts_ldap_name_attributes_title" = "Name attributes";
"settings_contacts_ldap_sip_attributes_title" = "SIP attributes";
"settings_contacts_ldap_sip_domain_title" = "SIP domain";
"settings_contacts_ldap_error_toast" = "A error occurred, LDAP server not saved!";
"settings_contacts_ldap_empty_server_error_toast" = "Server URL can't be empty";
"settings_contacts_title" = "Contacts";
"settings_conversations_auto_download_title" = "Auto-download files";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Mark conversation as read when dismissing message notification";
@ -510,6 +539,8 @@
"uri_handler_config_success_toast" = "Configuration successfully applied";
"username" = "Username";
"username_error" = "Username error";
"Voicemail" = "Voicemail";
"New message" = "New message";
"web_platform_forgotten_password_url" = "https://subscribe.linphone.org/";
"web_platform_register_email_url" = "https://subscribe.linphone.org/register/email";
"website_contact_url" = "https://linphone.org/contact";

View file

@ -0,0 +1,161 @@
"settings_calls_change_ringtone_title" = "Cambiar tono de llamada";
"settings_calls_echo_canceller_subtitle" = "Evita que el eco se escuche en el extremo remoto si no hay un cancelador de eco de hardware disponible";
"settings_calls_echo_canceller_title" = "Utilizar el cancelador de eco de software";
"settings_calls_enable_fec_title" = "Habilitar FEC de vídeo";
"settings_calls_enable_video_title" = "Habilitar video";
"settings_calls_title" = "Llamadas";
"settings_calls_vibrate_while_ringing_title" = "Vibrar mientras suena la llamada entrante";
"settings_contacts_add_carddav_server_title" = "Agregar libreta de direcciones CardDAV";
"settings_contacts_add_ldap_server_title" = "Agregar servidor LDAP";
"settings_contacts_carddav_name_title" = "Nombre para mostrar";
"settings_contacts_carddav_password_title" = "Clave";
"settings_contacts_carddav_server_url_title" = "URL del servidor";
"settings_contacts_carddav_sync_error_toast" = "Error de sincronización!";
"settings_contacts_carddav_username_title" = "Nombre de Usuario";
"settings_contacts_edit_carddav_server_title" = "Editar libreta de direcciones CardDAV";
"settings_contacts_edit_ldap_server_title" = "Editar servidor LDAP";
"settings_contacts_ldap_password_title" = "Clave";
"settings_contacts_title" = "Contactos";
"settings_conversations_auto_download_title" = "Descarga automática de archivos";
"dialog_no" = "No";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Marcar la conversación como leída al descartar la notificación del mensaje";
"settings_conversations_title" = "Conversaciones";
"settings_meetings_default_layout_title" = "Diseño predeterminado";
"settings_meetings_layout_active_speaker_label" = "Altavoz activo";
"settings_meetings_layout_mosaic_label" = "Mosaico";
"settings_meetings_title" = "Reuniones";
"settings_network_allow_ipv6" = "Permitir IPv6";
"settings_network_title" = "Red";
"settings_network_use_wifi_only" = "Use solo redes Wi-fi";
"settings_security_enable_vfs_subtitle" = "Advertencia: una vez habilitado, ¡no se puede deshabilitar!";
"settings_security_enable_vfs_title" = "Cifrar todo";
"settings_security_prevent_screenshots_title" = "Evitar que se grabe la interfaz";
"settings_security_title" = "Seguridad";
"settings_title" = "Ajustes";
"sip_address_copied_to_clipboard_toast" = "Dirección SIP copiada al portapapeles";
"sip_address_display_name" = "Nombre para mostrar";
"sip_address_domain" = "Dominio";
"start" = "Iniciar";
"uri_handler_config_success_toast" = "Configuración aplicada satisfactoriamente";
"username" = "Nombre de Usuario";
"welcome_page_2_title" = "Segura";
"welcome_page_3_title" = "código abierto";
"welcome_page_title" = "Bienvenido";
"account_settings_dialog_invalid_password_hint" = "Clave";
"account_settings_title" = "Configuración de la cuenta";
"assistant_account_create" = "Crear";
"assistant_account_creation_wrong_phone_number" = "Número equivocado?";
"assistant_account_login" = "Acceso";
"assistant_account_login_forbidden_error" = "Nombre de usuario o contraseña incorrectos";
"assistant_account_register" = "Registrar";
"assistant_account_register_push_notification_not_received_error" = "Notificación push con token de autenticación no recibida en 5 segundos, inténtelo nuevamente más tarde";
"assistant_account_register_unexpected_error" = "Se produjo un error inesperado, inténtelo de nuevo más tarde";
"assistant_already_have_an_account" = "Ya tienes una cuenta?";
"assistant_create_account_using_email_on_our_web_platform" = "Crea una cuenta con tu correo electrónico en:";
"assistant_dialog_confirm_phone_number_title" = "Confirmar número de teléfono";
"assistant_dialog_general_terms_and_privacy_policy_title" = "Términos generales y política de privacidad";
"assistant_dialog_general_terms_label" = "términos generales";
"assistant_dialog_privacy_policy_label" = "política de privacidad";
"assistant_login_third_party_sip_account" = "Utilice una cuenta SIP de terceros";
"assistant_no_account_yet" = "Aún no tienes cuenta?";
"assistant_permissions_grant_all_of_them" = "Ok";
"assistant_permissions_skip_permissions" = "Hazlo más tarde";
"assistant_permissions_title" = "Conceder permisos";
"assistant_qr_code_invalid_toast" = "Código QR no válido!";
"assistant_scan_qr_code" = "Escanear código QR";
"assistant_sip_account_transport_protocol" = "Transporte";
"assistant_third_party_sip_account_warning_ok" = "Yo entiendo";
"authentication_id" = "ID de autenticación (si es diferente)";
"bottom_navigation_calls_label" = "Llamadas";
"bottom_navigation_contacts_label" = "Contactos";
"bottom_navigation_conversations_label" = "Conversaciones";
"bottom_navigation_meetings_label" = "Reuniones";
"call_stats_media_encryption_title" = "Cifrado de medios";
"conference_layout_grid" = "Mosaico";
"contact_call_action" = "Llamar";
"contact_details_delete" = "Eliminar";
"conversation_action_call" = "Llamar";
"conversation_action_mark_as_read" = "Marcar como leído";
"conversation_ephemeral_messages_duration_disabled" = "Deshabilitado";
"dialog_accept" = "Aceptar";
"dialog_call" = "Llamar";
"dialog_cancel" = "Cancelar";
"dialog_continue" = "Continuar";
"dialog_deny" = "Denegar";
"dialog_install" = "Instalar";
"dialog_ok" = "Ok";
"dialog_yes" = "Si";
"drawer_menu_account_connection_status_cleared" = "Deshabilitado";
"drawer_menu_account_connection_status_connected" = "Conectado";
"drawer_menu_account_connection_status_failed" = "Error";
"drawer_menu_account_connection_status_progress" = "Conectando…";
"drawer_menu_add_account" = "Agregar una cuenta";
"drawer_menu_manage_account" = "Administrar el perfil";
"drawer_menu_no_account_configured_yet" = "Cuenta no configurada aun";
"Error" = "Error";
"help_about_advanced_title" = "Avanzado";
"help_about_check_for_update" = "Comprobar actualización";
"help_about_privacy_policy_title" = "Política de privacidad";
"help_about_user_guide_subtitle" = "Aprenda a dominar todas las funciones de la aplicación, paso a paso.";
"help_about_version_title" = "Versión";
"help_dialog_update_available_title" = "Actualización disponible";
"help_error_checking_version_toast_message" = "Se produjo un error al buscar actualizaciones";
"help_quit_title" = "Salir de la aplicación";
"help_title" = "Ayuda";
"help_troubleshooting_app_version_title" = "Versión de la aplicación";
"help_troubleshooting_clean_logs" = "Limpiar registros";
"help_troubleshooting_clear_native_friends_in_database" = "Borrar contactos importados de la libreta de direcciones nativa";
"help_troubleshooting_debug_logs_cleaned_toast_message" = "Se han limpiado los registros de depuración";
"help_troubleshooting_debug_logs_upload_error_toast_message" = "No se pudieron cargar los registros de depuración";
"help_troubleshooting_firebase_project_title" = "ID del proyecto de Firebase";
"help_troubleshooting_print_logs_in_logcat" = "Imprimir registros en logcat";
"help_troubleshooting_sdk_version_title" = "Versión SDK";
"help_troubleshooting_share_logs" = "Compartir registros";
"help_troubleshooting_share_logs_dialog_title" = "Compartir enlaces de registros de depuración usando…";
"help_troubleshooting_show_config_file" = "Mostrar configuracion";
"help_troubleshooting_title" = "Solución de problemas";
"help_version_up_to_date_toast_message" = "Su versión esta actualizada";
"Interoperable mode" = "Modo interoperable";
"manage_account_add_picture" = "Agregar una imagen";
"manage_account_delete" = "Cerrar sesión";
"manage_account_details_title" = "Detalles";
"manage_account_device_remove" = "Remover";
"manage_account_devices_title" = "Dispositivos";
"manage_account_edit_picture" = "Editar imagen";
"manage_account_international_prefix" = "Prefijo internacional";
"manage_account_no_device" = "No se encontró ningún dispositivo…";
"manage_account_remove_picture" = "Remover imagen";
"manage_account_settings" = "Configuración de la cuenta";
"manage_account_status_cleared_summary" = "La cuenta ha sido desactivada, no recibirás ninguna llamada ni mensaje.";
"manage_account_status_connected_summary" = "Esta cuenta está en línea, cualquiera puede llamarte.";
"manage_account_status_failed_summary" = "Error al conectar la cuenta, comprueba tu configuración.";
"manage_account_status_progress_summary" = "La cuenta se está conectando al servidor, por favor espere…";
"manage_account_title" = "Administrar cuenta";
"meeting_waiting_room_cancel" = "Cancelar";
"menu_delete_selected_item" = "Eliminar";
"menu_reply_to_chat_message" = "Responder";
"message_delivery_info_error_title" = "Error";
"next" = "Siguiente";
"notification_missed_call_title" = "Llamada perdida";
"or" = "o";
"password" = "Clave";
"phone_number" = "Número de teléfono";
"settings_advanced_accept_early_media_title" = "Acepta los primeros medios de comunicación";
"settings_advanced_allow_outgoing_early_media_title" = "Permitir la salida temprana de medios";
"settings_advanced_audio_codecs_title" = "Códecs de audio";
"settings_advanced_audio_devices_title" = "Dispositivos de audio";
"settings_advanced_device_id" = "Identificación de Dispositivo";
"settings_advanced_device_id_hint" = "Solo caracteres alfanuméricos";
"settings_advanced_download_apply_remote_provisioning" = "Descargar y aplicar";
"settings_advanced_input_audio_device_title" = "Dispositivo de audio de entrada predeterminado";
"settings_advanced_media_encryption_mandatory_title" = "Cifrado obligatorio de los medios de comunicación";
"settings_advanced_output_audio_device_title" = "Dispositivo de salida de audio predeterminado";
"settings_advanced_remote_provisioning_url" = "URL de aprovisionamiento remoto";
"settings_advanced_title" = "Configuración avanzada";
"settings_advanced_upload_server_url" = "URL del servidor para compartir archivos";
"settings_advanced_video_codecs_title" = "Códecs de video";
"settings_calls_auto_record_title" = "Iniciar automáticamente la grabación de llamadas";
"settings_calls_calibrate_echo_canceller_done_no_echo" = "sin eco";
"settings_calls_calibrate_echo_canceller_failed" = "fallido";
"settings_calls_calibrate_echo_canceller_in_progress" = "en progreso";
"settings_calls_calibrate_echo_canceller_title" = "Calibrar el cancelador de eco";

View file

@ -0,0 +1,16 @@
"next" = "Hurrengoa";
"account_settings_dialog_invalid_password_hint" = "Pasahitza";
"dialog_accept" = "Onartu";
"dialog_deny" = "Ukatu";
"or" = "edo";
"password" = "Pasahitza";
"phone_number" = "Telefono zenbakia";
"settings_advanced_device_id" = "Gailuaren IDa";
"settings_contacts_carddav_name_title" = "Erakusteko izena";
"settings_contacts_carddav_password_title" = "Pasahitza";
"settings_contacts_carddav_username_title" = "Erabiltzaile izena";
"settings_contacts_ldap_password_title" = "Pasahitza";
"sip_address_display_name" = "Erakusteko izena";
"sip_address_domain" = "Domeinua";
"start" = "Hasi";
"username" = "Erabiltzaile izena";

View file

@ -0,0 +1,31 @@
"account_settings_audio_video_conference_factory_uri_title" = "Audio/video kokous, tehdas URI";
"account_settings_dialog_invalid_password_title" = "Todennus tarvitaan";
": %@" = ": %@";
"[linphone.org/contact](https://linphone.org/contact)" = "[linphone.org/contact](https://linphone.org/contact)";
"*" = "*";
"**%@**" = "**%@**";
"#" = "#";
"%@" = "%@";
"%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";
"account_settings_avpf_title" = "AVPF";
"ZRTP" = "ZRTP";
"[https://sip.linphone.org](https://sip.linphone.org)" = "[https://sip.linphone.org](https://sip.linphone.org)";

View file

@ -47,6 +47,8 @@
"account_settings_im_encryption_mandatory_title" = "Chiffrement obligatoire des conversations";
"account_settings_lime_server_url_title" = "URL du serveur d'échange de clés de chiffrement";
"account_settings_mwi_uri_title" = "URI du serveur MWI (Message Waiting Indicator)";
"account_settings_apply_international_prefix_title" = "Formater les numéros en utilisant l'indicatif international";
"account_settings_replace_plus_by_00_title" = "Remplacer + par 00 lors du formatage des numéros de téléphone";
"account_settings_nat_policy_title" = "Paramètres de politique NAT";
"account_settings_outbound_proxy_title" = "Serveur mandataire sortant";
"account_settings_push_notification_not_available_title" = "Notifications push non disponibles";
@ -93,6 +95,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";
@ -187,6 +190,7 @@
"contacts_list_favourites_title" = "Favoris";
"contacts_list_filter_popup_see_all" = "Tous les contacts";
"contacts_list_filter_popup_see_linphone_only" = "Contacts %@";
"contacts_list_filter_popup_see_sip_only" = "Contacts SIP";
"conversation_action_call" = "Appeler";
"conversation_action_configure_ephemeral_messages" = "Configurer les messages éphémères";
"conversation_action_delete" = "Supprimer la conversation";
@ -200,6 +204,13 @@
"conversation_dialog_edit_subject" = "Renommer la conversation";
"conversation_dialog_set_subject" = "Nommer la conversation";
"conversation_dialog_subject_hint" = "Nom de la conversation";
"conversation_editing_message_title" = "Modification du message";
"conversation_message_edited_label" = "Modifié";
"conversation_dialog_delete_chat_message_title" = "Supprimer le message ?";
"conversation_dialog_delete_locally_label" = "Pour moi";
"conversation_dialog_delete_for_everyone_label" = "Pour tout le monde";
"conversation_message_content_deleted_label" = "Le message a été supprimé";
"conversation_message_content_deleted_by_us_label" = "Vous avez supprimé le message";
"conversation_end_to_end_encrypted_bottom_sheet_link" = "https://linphone.org/en/features/#security";
"conversation_end_to_end_encrypted_event_title" = "Conversation chiffrée de bout en bout";
"conversation_end_to_end_encrypted_event_subtitle" = "Les messages de cette conversation sont chiffrés de bout en bout. Seul votre correspondant peut les déchiffrer.";
@ -247,6 +258,8 @@
"conversation_message_meeting_cancelled_label" = "La réunion a été annulée";
"conversation_message_meeting_updated_label" = "La réunion a été mise à jour";
"conversation_one_to_one_hidden_subject" = "Dummy subject";
"conversation_participants_list_empty" = "Aucun participant trouvé";
"conversation_participants_list_header" = "Participants";
"conversation_reply_to_message_title" = "En réponse à : ";
"conversation_text_field_hint" = "Dites quelque chose…";
"conversations_list_empty" = "Aucune conversation pour le moment…";
@ -266,7 +279,7 @@
"dialog_deny" = "Refuser";
"dialog_install" = "Installer";
"dialog_no" = "Non";
"dialog_ok" = "OK";
"dialog_confirm" = "Confirmer";
"dialog_yes" = "Oui";
"drawer_menu_account_connection_status_cleared" = "Désactivé";
"drawer_menu_account_connection_status_connected" = "Connecté";
@ -337,7 +350,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.";
@ -390,21 +403,26 @@
"menu_delete_selected_item" = "Supprimer";
"menu_forward_chat_message" = "Transférer";
"menu_invite" = "Inviter";
"menu_edit_chat_message" = "Modifier";
"menu_reply_to_chat_message" = "Répondre";
"menu_resend_chat_message" = "Ré-envoyer";
"menu_see_existing_contact" = "Voir le contact";
"menu_show_imdn" = "Info de réception";
"menu_export_selected_item" = "Télécharger";
"menu_share_selected_item" = "Partager";
"message_copied_to_clipboard_toast" = "Message copié dans le presse-papier";
"message_delivery_info_error_title" = "En erreur";
"message_delivery_info_read_title" = "Lu";
"message_delivery_info_received_title" = "Reçu";
"message_delivery_info_sent_title" = "Envoyé";
"message_forwarded_label" = "Transféré";
"message_meeting_invitation_cancelled_notification" = "📅 Réunion annulée";
"message_meeting_invitation_notification" = "📅 Invitation à une réunion";
"message_meeting_invitation_updated_notification" = "📅 Réunion mise à jour";
"message_meeting_invitation_cancelled_notification" = "Réunion annulée";
"message_meeting_invitation_notification" = "Invitation à une réunion";
"message_meeting_invitation_updated_notification" = "Réunion mise à jour";
"message_reaction_click_to_remove_label" = "Cliquez pour supprimer";
"message_reactions_info_all_title" = "Réactions";
"mwi_messages_are_waiting_single" = "1 message vocal en attente";
"mwi_messages_are_waiting_multiple" = "%@ messages vocaux en attente";
"network_not_reachable" = "Vous nêtes pas connecté à internet";
"network_reachable_again" = "Vous êtes à nouveau connecté à internet";
"new_conversation_create_group" = "Créer une conversation de groupe";
@ -423,6 +441,7 @@
"picker_categories" = "Catégories";
"qr_code_validated" = "QR code validé";
"recordings_title" = "Enregistrements";
"recordings_list_empty" = "Aucun appel enregistré…";
"selected_participants_count" = "%@ participants selectionnés";
"settings_advanced_accept_early_media_title" = "Accepter l'early media";
"settings_advanced_allow_outgoing_early_media_title" = "Autoriser l'early media pour les appels sortants";
@ -466,15 +485,22 @@
"settings_contacts_carddav_username_title" = "Nom d'utilisateur";
"settings_contacts_edit_carddav_server_title" = "Editer le carnet d'adresse CardDAV";
"settings_contacts_edit_ldap_server_title" = "Editer le serveur LDAP";
"settings_contacts_ldap_enabled_title" = "Activé";
"settings_contacts_ldap_server_url_title" = "URL du serveur (ne peut être vide)";
"settings_contacts_ldap_bind_dn_title" = "Bind DN";
"settings_contacts_ldap_bind_user_password_title" = "Mot de passe de l'utilisateur Bind";
"settings_contacts_ldap_max_results_title" = "Nombre de résultats maximum";
"settings_contacts_ldap_password_title" = "Mot de passe";
"settings_contacts_ldap_request_timeout_title" = "Délai d'attente de la requête";
"settings_contacts_ldap_use_tls_title" = "Utiliser TLS";
"settings_contacts_ldap_search_base_title" = "Base de recherche (ne peut être vide)";
"settings_contacts_ldap_search_filter_title" = "Filtre";
"settings_contacts_ldap_server_url_title" = "URL du serveur (ne peut être vide)";
"settings_contacts_ldap_use_tls_title" = "Utiliser TLS";
"settings_contacts_ldap_max_results_title" = "Nombre de résultats maximum";
"settings_contacts_ldap_request_timeout_title" = "Durée maximum (en secondes)";
"settings_contacts_ldap_request_delay_title" = "Délai entre 2 requêtes (en millisecondes)";
"settings_contacts_ldap_min_characters_title" = "Nombre minimum de caractères pour lancer la requête";
"settings_contacts_ldap_name_attributes_title" = "Attributs de nom";
"settings_contacts_ldap_sip_attributes_title" = "Attributs SIP";
"settings_contacts_ldap_sip_domain_title" = "Domaine SIP";
"settings_contacts_ldap_error_toast" = "Une erreur s'est produite, la configuration LDAP n'a pas été sauvegardée !";
"settings_contacts_ldap_empty_server_error_toast" = "L'URL du serveur ne peut être vide";
"settings_contacts_title" = "Contacts";
"settings_conversations_auto_download_title" = "Télécharger automatiquement les fichiers";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Marquer la conversation comme lue lorsqu'une notification de message est supprimée";
@ -510,6 +536,8 @@
"uri_handler_config_success_toast" = "Configuration appliquée avec succè";
"username" = "Nom dutilisateur";
"username_error" = "Erreur dans le nom d'utilisateur";
"Voicemail" = "Messagerie vocale";
"New message" = "Nouveau message";
"web_platform_forgotten_password_url" = "https://subscribe.linphone.org/";
"web_platform_register_email_url" = "https://subscribe.linphone.org/register/email";
"website_contact_url" = "https://linphone.org/contact";

View file

@ -0,0 +1,86 @@
"account_settings_dialog_invalid_password_hint" = "Jelszó";
"assistant_account_create" = "Létrehoz";
"assistant_account_creation_wrong_phone_number" = "Rossz telefonszám?";
"assistant_account_login" = "Bejelentkezés";
"assistant_account_login_forbidden_error" = "Hibás felhasználónév vagy jelszó";
"assistant_account_register" = "Regisztráció";
"assistant_account_register_push_notification_not_received_error" = "A push értesítés az azonosító tokennel nem érkezett meg 5mp-en belül, próbálja meg később";
"assistant_account_register_unexpected_error" = "Váratlan hiba történt, próbálja meg később";
"assistant_already_have_an_account" = "Már van fiókja?";
"assistant_create_account_using_email_on_our_web_platform" = "Hozzon létre egy fiókot az emailcímével:";
"assistant_dialog_confirm_phone_number_title" = "Erősítse meg a telefonszámot";
"assistant_dialog_general_terms_and_privacy_policy_title" = "Általános feltételek és biztonsági szabályok";
"assistant_dialog_general_terms_label" = "általános feltételek";
"assistant_dialog_privacy_policy_label" = "biztonsági szabályok";
"assistant_login_third_party_sip_account" = "Harmadik féltől származó SIP fiók használata";
"assistant_no_account_yet" = "Nincs még fiókja?";
"assistant_permissions_grant_all_of_them" = "OK";
"assistant_permissions_skip_permissions" = "Később";
"assistant_permissions_title" = "Engedélyek biztosítása";
"assistant_qr_code_invalid_toast" = "Érvénytelen QRkód!";
"assistant_scan_qr_code" = "QRkód beolvasása";
"assistant_sip_account_transport_protocol" = "SIP átvitel";
"assistant_third_party_sip_account_warning_ok" = "Elfogadom";
"authentication_id" = "Azonosító ID (ha különbözik)";
"bottom_navigation_calls_label" = "Hívások";
"bottom_navigation_contacts_label" = "Névjegyek";
"bottom_navigation_conversations_label" = "Üzenetek";
"bottom_navigation_meetings_label" = "Meetingek";
"contact_call_action" = "Hívás";
"contact_details_delete" = "Törlés";
"conversation_action_call" = "Hívás";
"conversation_action_mark_as_read" = "Jelölés olvasottnak";
"conversation_ephemeral_messages_duration_disabled" = "Letiltva";
"dialog_accept" = "Elfogad";
"dialog_call" = "Hívás";
"dialog_cancel" = "Mégse";
"dialog_continue" = "Folytatás";
"dialog_deny" = "Elutasít";
"dialog_install" = "Telepítés";
"dialog_no" = "Nem";
"dialog_ok" = "OK";
"dialog_yes" = "Igen";
"drawer_menu_account_connection_status_cleared" = "Letiltva";
"drawer_menu_account_connection_status_connected" = "Csatlakoztatva";
"drawer_menu_account_connection_status_failed" = "Hiba";
"drawer_menu_account_connection_status_progress" = "Csatlakozás…";
"drawer_menu_add_account" = "Fiók hozzáadása";
"drawer_menu_manage_account" = "Profil kezelése";
"drawer_menu_no_account_configured_yet" = "Nincs még fiók konfigurálva";
"Error" = "Hiba";
"help_about_advanced_title" = "Haladó";
"help_about_version_title" = "Verzió";
"help_title" = "Segítség";
"help_troubleshooting_clear_native_friends_in_database" = "Importált névjegyek törlése a telefon névjegyzékéből";
"help_troubleshooting_show_config_file" = "Mutassa a konfigurációt";
"manage_account_device_remove" = "Eltávolít";
"meeting_waiting_room_cancel" = "Mégse";
"menu_delete_selected_item" = "Törlés";
"menu_reply_to_chat_message" = "Válasz";
"message_delivery_info_error_title" = "Hiba";
"next" = "Következő";
"notification_missed_call_title" = "Nem fogadott hívás";
"or" = "vagy";
"password" = "Jelszó";
"phone_number" = "Telefonszám";
"settings_advanced_device_id" = "Eszköz ID";
"settings_calls_title" = "Hívások";
"settings_contacts_carddav_name_title" = "Megjelenített név";
"settings_contacts_carddav_password_title" = "Jelszó";
"settings_contacts_carddav_username_title" = "Felhasználónév";
"settings_contacts_ldap_password_title" = "Jelszó";
"settings_contacts_title" = "Névjegyek";
"settings_conversations_title" = "Üzenetek";
"settings_meetings_title" = "Meetingek";
"settings_security_enable_vfs_title" = "Titkosítás";
"settings_security_title" = "Biztonság";
"settings_title" = "Beállítások";
"sip_address_copied_to_clipboard_toast" = "SIP cím a vágólapra másolva";
"sip_address_display_name" = "Megjelenített név";
"sip_address_domain" = "Domain cím";
"start" = "Indítás";
"uri_handler_config_success_toast" = "A konfiguráció sikeresen alkalmazva";
"username" = "Felhasználónév";
"welcome_page_2_title" = "Biztonságos";
"welcome_page_3_title" = "Nyílt forráskódú";
"welcome_page_title" = "Üdvözöljük";

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,363 @@
"dialog_yes" = "Ja";
"Error" = "Fout";
"menu_copy_chat_message" = "Kopiëren";
"account_settings_audio_video_conference_factory_uri_title" = "URI van de Audio/video conference factory";
"account_settings_avpf_title" = "AVPF";
"account_settings_bundle_mode_title" = "Bundelmodus";
"account_settings_ccmp_server_url_title" = "URL van de CCMP-server";
"account_settings_conference_factory_uri_title" = "Conference factory URI";
"account_settings_cpim_in_basic_conversations_title" = "Gebruik CPIM in 'basis' gesprekken";
"account_settings_dialog_invalid_password_title" = "Authenticatie vereist";
"account_settings_dialog_invalid_password_hint" = "Wachtwoord";
"account_settings_enable_ice_title" = "Schakel ICE in";
"account_settings_enable_turn_title" = "Schakel TURN in";
"account_settings_expire_title" = "Vervalt (in seconden)";
"account_settings_im_encryption_mandatory_title" = "IM-versleuteling verplicht";
"account_settings_lime_server_url_title" = "URL van de E2E-versleuteling sleutel server";
"account_settings_mwi_uri_title" = "MWI-server URI (Message Waiting Indicator)";
"account_settings_nat_policy_title" = "NAT-beleid instellingen";
"account_settings_outbound_proxy_title" = "Uitgaande proxy";
"account_settings_push_notification_not_available_title" = "Pushmeldingen zijn niet beschikbaar!";
"account_settings_push_notification_title" = "Sta pushmeldingen toe";
"account_settings_sip_proxy_url_title" = "URL van de SIP-proxyserver";
"account_settings_stun_server_url_title" = "URL van de STUN/TURN-server";
"account_settings_title" = "Accountinstellingen";
"account_settings_turn_password_title" = "TURN-wachtwoord";
"account_settings_turn_username_title" = "TURN-gebruikersnaam";
"account_settings_update_password_title" = "Wachtwoord bijwerken";
"account_settings_voicemail_uri_title" = "Voicemail-URI";
"assistant_account_create" = "Maken";
"assistant_account_creation_wrong_phone_number" = "Verkeerd nummer?";
"assistant_account_login" = "Login";
"assistant_account_login_forbidden_error" = "Verkeerde gebruikersnaam of wachtwoord";
"assistant_account_register" = "Registreren";
"assistant_account_register_push_notification_not_received_error" = "Pushmelding met autorisatietoken niet binnen 5 seconden ontvangen, probeer het later opnieuw";
"assistant_account_register_unexpected_error" = "Er is een onverwachte fout opgetreden, probeer het later opnieuw";
"assistant_already_have_an_account" = "Heeft u al een account?";
"assistant_create_account_using_email_on_our_web_platform" = "Maak een account aan met uw e-mailadres op:";
"assistant_dialog_confirm_phone_number_title" = "Bevestig telefoonnummer";
"assistant_dialog_general_terms_and_privacy_policy_title" = "Algemene voorwaarden & privacybeleid";
"assistant_dialog_general_terms_label" = "algemene voorwaarden";
"assistant_dialog_privacy_policy_label" = "privacy beleid";
"assistant_login_third_party_sip_account" = "Gebruik een SIP-account van derden";
"assistant_no_account_yet" = "Nog geen account?";
"assistant_permissions_grant_all_of_them" = "OK";
"assistant_permissions_skip_permissions" = "Doe het later";
"assistant_permissions_title" = "Verleen machtigingen";
"assistant_qr_code_invalid_toast" = "Ongeldige QR-code!";
"assistant_scan_qr_code" = "Scan de QR-code";
"assistant_sip_account_transport_protocol" = "Vervoer";
"assistant_third_party_sip_account_warning_ok" = "Ik begrijp";
"authentication_id" = "Authenticatie-ID (indien anders)";
"bottom_navigation_calls_label" = "Oproepen";
"bottom_navigation_contacts_label" = "Contacten";
"bottom_navigation_conversations_label" = "Gesprekken";
"bottom_navigation_meetings_label" = "Vergaderingen";
"call_action_blind_transfer" = "Doorverbinden";
"call_action_change_layout" = "Indeling";
"call_action_go_to_calls_list" = "Oproeplijst";
"call_action_hang_up" = "Oproep beëindigen";
"call_action_pause_call" = "Pauzeren";
"call_action_record_call" = "Opnemen";
"call_action_resume_call" = "Hervatten";
"call_action_show_dialer" = "Toetsenbord";
"call_action_show_messages" = "Berichten";
"call_action_start_new_call" = "Nieuwe oproep";
"call_audio_device_type_earpiece" = "Oortelefoon";
"call_audio_device_type_headphones" = "Koptelefoon";
"call_audio_device_type_speaker" = "Spreker";
"call_audio_incoming" = "Inkomende oproep";
"call_dialog_zrtp_security_alert_message" = "De vertrouwelijkheid van deze oproep kan in gevaar zijn!";
"call_dialog_zrtp_security_alert_title" = "Beveiligingswaarschuwing";
"call_dialog_zrtp_security_alert_try_again" = "Probeer het opnieuw";
"call_dialog_zrtp_validate_trust_letters_do_not_match" = "Niets komt overeen";
"call_dialog_zrtp_validate_trust_local_code_label" = "Uw code:";
"call_dialog_zrtp_validate_trust_remote_code_label" = "Code van gesprekspartner:";
"call_dialog_zrtp_validate_trust_title" = "Valideer het apparaat";
"call_do_zrtp_sas_validation_again" = "Valideer ZRTP SAS opnieuw";
"call_history_deleted_toast" = "Gespreksgeschiedenis is verwijderd";
"call_not_encrypted" = "Oproep is niet versleuteld";
"call_outgoing" = "Uitgaande oproep";
"call_srtp_point_to_point_encrypted" = "Punt-naar-punt versleuteld door SRTP";
"call_state_connected" = "Actief";
"call_state_paused" = "Gepauzeerd";
"call_state_paused_by_remote" = "Gepauzeerd door externe partij";
"call_state_resuming" = "Hervatten…";
"call_stats_audio_title" = "Audio";
"call_stats_media_encryption_title" = "Media-versleuteling";
"call_stats_video_title" = "Video";
"call_transfer_failed_toast" = "Doorverbinden van oproep mislukt!";
"call_transfer_in_progress_toast" = "Oproep wordt doorverbonden";
"call_transfer_successful_toast" = "Oproep is succesvol doorverbonden";
"call_waiting_for_encryption_info" = "Wachten op versleuteling…";
"call_zrtp_end_to_end_encrypted" = "Eind-tot-eind versleuteld door ZRTP";
"call_zrtp_sas_validation_required" = "Validatie vereist";
"calls_list_dialog_merge_into_conference_label" = "Groepsgesprek aanmaken";
"calls_list_dialog_merge_into_conference_title" = "Alle oproepen samenvoegen in een groepsgesprek ?";
"calls_list_title" = "Oproeplijst";
"conference_action_screen_sharing" = "Scherm delen";
"conference_action_show_participants" = "Deelnemers";
"conference_call_empty" = "Wachten op andere deelnemers…";
"conference_failed_to_create_group_call_toast" = "Groepsoproep aanmaken mislukt!";
"conference_layout_active_speaker" = "Spreker";
"conference_layout_audio_only" = "Alleen audio";
"conference_layout_grid" = "Mozaïek";
"conference_participant_joining_text" = "Deelnemen…";
"conference_participant_paused_text" = "Gepauzeerd";
"conference_share_link_title" = "Uitnodiging delen";
"connection_error_for_non_default_account" = "Account(s) verbindingsfout";
"contact_call_action" = "Telefoongesprek";
"contact_details_actions_title" = "Andere acties";
"contact_details_add_to_favourites" = "Toevoegen aan favorieten";
"contact_details_delete" = "Wissen";
"contact_details_edit" = "Bewerken";
"contact_details_remove_from_favourites" = "Verwijderen uit favorieten";
"contact_details_share" = "Delen";
"contact_dialog_delete_message" = "Dit contact wordt definitief verwijderd.";
"contact_dialog_pick_phone_number_or_sip_address_title" = "Kies een nummer of een SIP-adres";
"contact_edit_title" = "Bewerk contact";
"contact_editor_company" = "Bedrijf";
"contact_editor_dialog_abort_confirmation_message" = "Alle wijzigingen gaan verloren";
"contact_editor_dialog_abort_confirmation_title" = "Wijzigingen niet opslaan?";
"contact_editor_first_name" = "Voornaam";
"contact_editor_job_title" = "Functietitel";
"contact_editor_last_name" = "Achternaam";
"contact_message_action" = "Bericht";
"contact_new_title" = "Nieuw contact";
"contacts_list_all_contacts_title" = "Alle contacten";
"contacts_list_empty" = "Geen contact op dit moment…";
"contacts_list_favourites_title" = "Favorieten";
"contacts_list_filter_popup_see_all" = "Zie alles";
"conversation_action_call" = "Telefoongesprek";
"conversation_action_configure_ephemeral_messages" = "Configureer kortstondige berichten";
"conversation_action_delete" = "Gesprek verwijderen";
"conversation_action_leave_group" = "Groep verlaten";
"conversation_action_mark_as_read" = "Markeer als gelezen";
"conversation_action_mute" = "Dempen";
"conversation_action_unmute" = "Dempen uitschakelen";
"conversation_add_participants_title" = "Deelnemers toevoegen";
"conversation_dialog_edit_subject" = "Gespreksonderwerp bewerken";
"conversation_dialog_set_subject" = "Gespreksonderwerp instellen";
"conversation_dialog_subject_hint" = "Gespreksonderwerp";
"conversation_end_to_end_encrypted_event_title" = "Eind-tot-eind versleuteld gesprek";
"conversation_end_to_end_encrypted_event_subtitle" = "Berichten in dit gesprek zijn eind-tot-eind versleuteld. Alleen je gesprekspartner kan ze decoderen.";
"conversation_ephemeral_messages_duration_disabled" = "Uitgeschakeld";
"conversation_ephemeral_messages_duration_one_day" = "1 dag";
"conversation_ephemeral_messages_duration_one_hour" = "1 uur";
"conversation_ephemeral_messages_duration_one_minute" = "1 minuut";
"conversation_ephemeral_messages_duration_one_week" = "1 week";
"conversation_ephemeral_messages_duration_three_days" = "3 dagen";
"conversation_ephemeral_messages_subtitle" = "Nieuwe berichten worden automatisch verwijderd zodra ze door iedereen zijn gelezen.\nKies een duur:";
"conversation_ephemeral_messages_title" = "Kortstondige berichten";
"conversation_event_conference_created" = "Je bent bij de groep gekomen";
"conversation_event_conference_destroyed" = "Je hebt de groep verlaten";
"conversation_event_ephemeral_messages_disabled" = "Tijdelijke berichten zijn uitgeschakeld";
"conversation_event_ephemeral_messages_enabled" = "Tijdelijke berichten zijn ingeschakeld";
"conversation_failed_to_create_toast" = "Gesprek aanmaken mislukt!";
"conversation_forward_message_title" = "Bericht doorsturen naar…";
"conversation_info_add_participants_label" = "Deelnemers toevoegen";
"conversation_info_admin_menu_remove_participant" = "Verwijderen uit de groep";
"conversation_info_admin_menu_set_participant_admin" = "Beheerrechten toekennen";
"conversation_info_admin_menu_unset_participant_admin" = "Beheerrechten intrekken";
"conversation_info_confirm_start_group_call_dialog_message" = "Alle deelnemers ontvangen een oproep.";
"conversation_info_confirm_start_group_call_dialog_title" = "Groepsoproep starten?";
"conversation_info_delete_history_action" = "Geschiedenis verwijderen";
"conversation_info_menu_add_to_contacts" = "Toevoegen aan contacten";
"conversation_info_menu_go_to_contact" = "Bekijk contactprofiel";
"conversation_info_participant_is_admin_label" = "Beheerder";
"conversation_invalid_participant_due_to_security_mode_toast" = "Gesprek kan niet worden aangemaakt met een deelnemer die zich niet op hetzelfde domein bevindt vanwege beveiligingsbeperkingen!";
"conversation_menu_configure_ephemeral_messages" = "Kortstondige berichten";
"conversation_menu_go_to_info" = "Gesprek informatie";
"conversation_message_forward_cancelled_toast" = "Doorsturen van bericht is geannuleerd";
"conversation_message_forwarded_toast" = "Bericht is doorgestuurd";
"conversation_message_meeting_updated_label" = "Vergadering is bijgewerkt";
"conversation_text_field_hint" = "Zeg iets…";
"conversations_list_empty" = "Geen gesprek op dit moment…";
"conversation_take_picture_label" = "Foto maken";
"conversation_pick_file_from_gallery_label" = "Galerij openen";
"conversation_pick_any_file_label" = "Bestand kiezen";
"conversation_file_cant_be_opened_error_toast" = "Bestand kan niet worden geopend!";
"default_account_disabled" = "Geselecteerd account is momenteel uitgeschakeld";
"dialog_accept" = "Accepteren";
"dialog_call" = "Telefoongesprek";
"dialog_cancel" = "Annuleren";
"dialog_continue" = "Doorgaan";
"dialog_deny" = "Ontkennen";
"dialog_install" = "Installeren";
"dialog_no" = "Nee";
"dialog_ok" = "OK";
"drawer_menu_account_connection_status_cleared" = "Uitgeschakeld";
"drawer_menu_account_connection_status_connected" = "Verbonden";
"drawer_menu_account_connection_status_failed" = "Fout";
"drawer_menu_account_connection_status_progress" = "Verbinden…";
"drawer_menu_add_account" = "Voeg een account toe";
"drawer_menu_manage_account" = "Beheer het profiel";
"drawer_menu_no_account_configured_yet" = "Nog geen account geconfigureerd";
"generic_address_picker_suggestions_list_title" = "Suggesties";
"help_about_advanced_title" = "Geavanceerd";
"help_about_check_for_update" = "Controleer op updates";
"help_about_privacy_policy_title" = "Privacy beleid";
"help_about_version_title" = "Versie";
"help_dialog_update_available_title" = "Update beschikbaar";
"help_error_checking_version_toast_message" = "Er is een fout opgetreden tijdens het controleren op updates";
"help_quit_title" = "Afsluiten";
"help_title" = "Help";
"help_troubleshooting_app_version_title" = "App-versie";
"help_troubleshooting_clean_logs" = "Logboeken opschonen";
"help_troubleshooting_clear_native_friends_in_database" = "Geïmporteerde contacten uit het lokale adresboek verwijderen";
"help_troubleshooting_debug_logs_cleaned_toast_message" = "Debuglogs zijn opgeschoond";
"help_troubleshooting_debug_logs_upload_error_toast_message" = "Uploaden van debuglogs mislukt";
"help_troubleshooting_firebase_project_title" = "Firebase-project-ID";
"help_troubleshooting_print_logs_in_logcat" = "Logboeken afdrukken in logcat";
"help_troubleshooting_sdk_version_title" = "SDK-versie";
"help_troubleshooting_share_logs" = "Logs delen";
"help_troubleshooting_share_logs_dialog_title" = "Deel link naar foutopsporingslogs via…";
"help_troubleshooting_show_config_file" = "Configuratie weergeven";
"help_troubleshooting_title" = "Probleemoplossing";
"help_version_up_to_date_toast_message" = "Uw versie is up-to-date";
"history_call_start_create_group_call" = "Maak een groepsgesprek";
"history_call_start_search_bar_filter_hint" = "Zoek contact of gespreksgeschiedenis";
"history_call_start_title" = "Nieuwe oproep";
"history_dialog_delete_all_call_logs_message" = "Alle oproepen worden uit de geschiedenis verwijderd";
"history_dialog_delete_all_call_logs_title" = "Weet je zeker dat je alle gespreksgeschiedenis wilt verwijderen?";
"history_group_call_start_dialog_set_subject" = "Instellen onderwerp groepsoproep";
"history_group_call_start_dialog_subject_hint" = "Onderwerp groepsoproep";
"history_list_empty_history" = "Geen oproep op dit moment…";
"Interoperable mode" = "Interoperabele modus";
"list_filter_no_result_found" = "Geen resultaat gevonden…";
"manage_account_add_picture" = "Voeg een foto toe";
"manage_account_delete" = "Uitloggen";
"manage_account_details_title" = "Details";
"manage_account_device_last_connection" = "Laatste verbinding:";
"manage_account_device_remove" = "Weghalen";
"manage_account_devices_title" = "Apparaten";
"manage_account_dialog_remove_account_message" = "Als je je account permanent wilt verwijderen, ga naar: https://sip.linphone.org";
"manage_account_dialog_remove_account_title" = "Uitloggen uit je account?";
"manage_account_edit_picture" = "Bewerk foto";
"manage_account_international_prefix" = "Internationale prefix";
"manage_account_no_device" = "Geen apparaat gevonden…";
"manage_account_remove_picture" = "Verwijder foto";
"manage_account_settings" = "Accountinstellingen";
"manage_account_status_cleared_summary" = "Account is uitgeschakeld, je ontvangt geen oproepen of berichten.";
"manage_account_status_connected_summary" = "Dit account is online, iedereen kan je bellen.";
"manage_account_status_failed_summary" = "Verbinding met account mislukt, controleer je instellingen.";
"manage_account_status_progress_summary" = "Account is verbinding aan het maken met de server, even geduld aub…";
"manage_account_title" = "Account beheren";
"meeting_failed_to_schedule_toast" = "Vergadering inplannen mislukt!";
"meeting_failed_to_send_invites_toast" = "Het verzenden van alle uitnodigingen voor de vergadering is mislukt!";
"meeting_failed_to_send_part_of_invites_toast" = "Het verzenden van uitnodigingen naar sommige deelnemers van de vergadering is mislukt!";
"meeting_info_cancelled_toast" = "Vergadering is geannuleerd";
"meeting_info_created_toast" = "Vergadering is aangemaakt";
"meeting_info_delete" = "Vergadering verwijderen";
"meeting_info_deleted_toast" = "Vergadering is verwijderd";
"meeting_info_export_as_calendar_event" = "Agenda-item aanmaken";
"meeting_info_join_title" = "Sluit nu aan in de vergadering";
"meeting_info_organizer_label" = "Organisator";
"meeting_info_updated_toast" = "Vergadering is bijgewerkt";
"meeting_schedule_add_participants_title" = "Deelnemers toevoegen";
"meeting_schedule_cancel_dialog_message" = "Wil je de vergadering annuleren en een melding naar alle deelnemers sturen?";
"meeting_schedule_cancel_dialog_title" = "Vergadering annuleren?";
"meeting_schedule_description_hint" = "Beschrijving toevoegen";
"meeting_schedule_description_title" = "Beschrijving";
"meeting_schedule_edit_title" = "Vergadering bewerken";
"meeting_schedule_meeting_label" = "Vergadering";
"meeting_schedule_pick_end_time_title" = "Kies de eindtijd";
"meeting_schedule_pick_start_date_title" = "Kies de startdatum";
"meeting_schedule_pick_start_time_title" = "Kies de starttijd";
"meeting_schedule_send_invitations_title" = "Stuur uitnodiging naar deelnemers";
"meeting_schedule_subject_hint" = "Titel toevoegen…";
"meeting_schedule_timezone_title" = "Tijdzone";
"meeting_schedule_title" = "Nieuwe vergadering";
"meeting_waiting_room_cancel" = "Annuleren";
"meeting_waiting_room_join" = "Deelnemen";
"meeting_waiting_room_joining_title" = "Verbinding wordt tot stand gebracht";
"meetings_list_no_meeting_for_today" = "Er is geen vergadering gepland voor vandaag";
"menu_add_address_to_contacts" = "Toevoegen aan contacten";
"menu_copy_phone_number" = "Telefoonnummer kopiëren";
"menu_delete_history" = "Geschiedenis verwijderen";
"menu_delete_selected_item" = "Wissen";
"menu_forward_chat_message" = "Doorsturen";
"menu_invite" = "Uitnodigen";
"menu_reply_to_chat_message" = "Antwoord";
"menu_resend_chat_message" = "Opnieuw verzenden";
"menu_see_existing_contact" = "Bekijk contact";
"menu_show_imdn" = "Bezorgstatus";
"message_delivery_info_error_title" = "Fout";
"message_forwarded_label" = "Doorgestuurd";
"message_reaction_click_to_remove_label" = "Klik om te verwijderen";
"network_not_reachable" = "Je bent niet verbonden met internet";
"new_conversation_create_group" = "Maak een groepsgesprek";
"new_conversation_search_bar_filter_hint" = "Zoek contact";
"new_conversation_title" = "Nieuw gesprek";
"next" = "Volgende";
"notification_missed_call_title" = "Gemiste oproep";
"operation_in_progress_overlay" = "Bezig met bewerking, even geduld alstublieft";
"or" = "of";
"password" = "Wachtwoord";
"phone_number" = "Telefoonnummer";
"recordings_title" = "Opnames";
"settings_advanced_accept_early_media_title" = "Accepteer vroege media";
"settings_advanced_allow_outgoing_early_media_title" = "Sta uitgaande vroege media toe";
"settings_advanced_audio_codecs_title" = "Audiocodecs";
"settings_advanced_audio_devices_title" = "Audioapparaten";
"settings_advanced_device_id" = "Apparaat-ID";
"settings_advanced_device_id_hint" = "Alleen alfanumerieke tekens";
"settings_advanced_download_apply_remote_provisioning" = "Downloaden en toepassen";
"settings_advanced_input_audio_device_title" = "Standaard invoer audioapparaat";
"settings_advanced_media_encryption_mandatory_title" = "Media-versleuteling verplicht";
"settings_advanced_output_audio_device_title" = "Standaard uitvoer audioapparaat";
"settings_advanced_remote_provisioning_url" = "URL voor externe provisioning";
"settings_advanced_title" = "Geavanceerde instellingen";
"settings_advanced_upload_server_url" = "URL van de bestanddelingsserver";
"settings_advanced_video_codecs_title" = "Video-codecs";
"settings_calls_adaptive_rate_control_title" = "Adaptieve bitrate-regeling";
"settings_calls_auto_record_title" = "Oproepen automatisch opnemen";
"settings_calls_calibrate_echo_canceller_done_no_echo" = "Geen echo";
"settings_calls_calibrate_echo_canceller_failed" = "Mislukt";
"settings_calls_calibrate_echo_canceller_in_progress" = "In behandeling";
"settings_calls_calibrate_echo_canceller_title" = "Echo-onderdrukking kalibreren";
"settings_calls_change_ringtone_title" = "Beltoon wijzigen";
"settings_calls_echo_canceller_subtitle" = "Voorkomt echo aan de andere kant van de lijn als er geen hardwarematige echo-onderdrukking beschikbaar is";
"settings_calls_echo_canceller_title" = "Softwarematige echo-onderdrukking gebruiken";
"settings_calls_enable_fec_title" = "Video-FEC inschakelen";
"settings_calls_enable_video_title" = "Video inschakelen";
"settings_calls_title" = "Oproepen";
"settings_calls_vibrate_while_ringing_title" = "Trillen tijdens inkomend gesprek";
"settings_contacts_add_carddav_server_title" = "Voeg CardDAV-adresboek toe";
"settings_contacts_add_ldap_server_title" = "LDAP-server toevoegen";
"settings_contacts_carddav_name_title" = "Weergavenaam";
"settings_contacts_carddav_password_title" = "Wachtwoord";
"settings_contacts_carddav_server_url_title" = "Server-URL";
"settings_contacts_carddav_sync_error_toast" = "Synchronisatiefout!";
"settings_contacts_carddav_username_title" = "Gebruikersnaam";
"settings_contacts_edit_carddav_server_title" = "Bewerk CardDAV-adresboek";
"settings_contacts_edit_ldap_server_title" = "Bewerk LDAP-server";
"settings_contacts_ldap_bind_dn_title" = "Bind-DN";
"settings_contacts_ldap_password_title" = "Wachtwoord";
"settings_contacts_ldap_server_url_title" = "Server-URL (mag niet leeg zijn)";
"settings_contacts_ldap_use_tls_title" = "TLS gebruiken";
"settings_contacts_title" = "Contacten";
"settings_conversations_auto_download_title" = "Bestanden automatisch downloaden";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Gesprek als gelezen markeren bij het sluiten van berichtmelding";
"settings_conversations_title" = "Gesprekken";
"settings_meetings_default_layout_title" = "Standaardindeling";
"settings_meetings_layout_active_speaker_label" = "Actieve spreker";
"settings_meetings_layout_mosaic_label" = "Mozaïek";
"settings_meetings_title" = "Vergaderingen";
"settings_network_allow_ipv6" = "Sta IPv6 toe";
"settings_network_title" = "Netwerk";
"settings_network_use_wifi_only" = "Gebruik alleen Wi-Fi-netwerken";
"settings_security_enable_vfs_subtitle" = "Waarschuwing: eenmaal ingeschakeld kan dit niet meer worden uitgeschakeld!";
"settings_security_enable_vfs_title" = "Versleutel alles";
"settings_security_prevent_screenshots_title" = "Voorkom dat de interface wordt opgenomen";
"settings_security_title" = "Beveiliging";
"settings_title" = "Instellingen";
"sip_address_copied_to_clipboard_toast" = "SIP-adres gekopieerd naar klembord";
"sip_address_display_name" = "Weergavenaam";
"sip_address_domain" = "Domein";
"start" = "Begin";
"uri_handler_config_success_toast" = "Configuratie succesvol toegepast";
"username" = "Gebruikersnaam";
"welcome_page_2_title" = "Beveiligd";
"welcome_page_3_title" = "Open-source";
"welcome_page_title" = "Welkom";

View file

@ -0,0 +1,525 @@
"assistant_account_create" = "Criar";
"assistant_account_creation_wrong_phone_number" = "Número errado?";
"assistant_account_login" = "Login";
"assistant_account_login_forbidden_error" = "Nome de usuário ou senha incorretos";
"assistant_account_register" = "Registrar";
"assistant_account_register_push_notification_not_received_error" = "Notificação push com token de autenticação não recebida em 5 segundos, por favor, tente novamente mais tarde";
"assistant_account_register_unexpected_error" = "Ocorreu um erro inesperado, por favor, tente novamente mais tarde";
"conversation_ephemeral_messages_duration_disabled" = "Desativado";
"conversation_invalid_participant_due_to_security_mode_toast" = "Não é possível criar conversa com um participante que não está no mesmo domínio devido a restrições de segurança!";
"conversation_menu_configure_ephemeral_messages" = "Mensagens efêmeras";
"conversation_menu_go_to_info" = "Informações da conversa";
"conversation_message_forward_cancelled_toast" = "Encaminhamento de mensagem cancelado";
"conversations_list_empty" = "Nenhuma conversa no momento…";
"default_account_disabled" = "A conta selecionada está atualmente desativada";
"dialog_accept" = "Aceitar";
"dialog_call" = "Chamar";
"dialog_cancel" = "Cancelar";
"dialog_continue" = "Continuar";
"dialog_deny" = "Recusar";
"dialog_install" = "Instalar";
"Error" = "Erro";
"help_troubleshooting_clean_logs" = "Limpar logs";
"help_troubleshooting_clear_native_friends_in_database" = "Limpar contatos importados da agenda nativa";
"help_troubleshooting_debug_logs_cleaned_toast_message" = "Logs de depuração foram limpos";
"help_troubleshooting_debug_logs_upload_error_toast_message" = "Falha ao enviar logs de depuração";
"help_troubleshooting_firebase_project_title" = "ID do projeto Firebase";
"help_troubleshooting_print_logs_in_logcat" = "Imprimir logs no logcat";
"help_troubleshooting_sdk_version_title" = "Versão do SDK";
"help_troubleshooting_share_logs" = "Compartilhar logs";
"help_troubleshooting_share_logs_dialog_title" = "Compartilhar link de logs de depuração usando…";
"help_troubleshooting_show_config_file" = "Mostrar configuração";
"help_troubleshooting_title" = "Solução de problemas";
"help_version_up_to_date_toast_message" = "Sua versão está atualizada";
"history_call_start_create_group_call" = "Criar uma chamada em grupo";
"history_call_start_search_bar_filter_hint" = "Pesquisar contato ou histórico de chamadas";
"history_call_start_title" = "Nova chamada";
"history_dialog_delete_all_call_logs_message" = "Todas as chamadas serão removidas do histórico";
"history_dialog_delete_all_call_logs_title" = "Você realmente quer excluir todo o histórico de chamadas?";
"history_group_call_start_dialog_set_subject" = "Definir assunto da chamada em grupo";
"history_group_call_start_dialog_subject_hint" = "Assunto da chamada em grupo";
"history_list_empty_history" = "Nenhuma chamada no momento…";
"Interoperable mode" = "Modo interoperável";
"list_filter_no_result_found" = "Nenhum resultado encontrado…";
"manage_account_no_device" = "Nenhum dispositivo encontrado…";
"manage_account_remove_picture" = "Remover foto";
"manage_account_settings" = "Configurações da conta";
"manage_account_status_cleared_summary" = "A conta foi desativada, você não receberá nenhuma chamada ou mensagem.";
"manage_account_status_connected_summary" = "Esta conta está online, todos podem ligar para você.";
"manage_account_status_failed_summary" = "Falha na conexão da conta, verifique suas configurações.";
"meeting_failed_to_send_invites_toast" = "Falha ao enviar todos os convites para a reunião!";
"meeting_failed_to_send_part_of_invites_toast" = "Falha ao enviar convites para alguns participantes da reunião!";
"meeting_info_cancelled_toast" = "A reunião foi cancelada";
"meeting_info_created_toast" = "A reunião foi criada";
"meeting_info_delete" = "Excluir reunião";
"meeting_info_organizer_label" = "Organizador";
"meeting_info_updated_toast" = "A reunião foi atualizada";
"meeting_schedule_add_participants_title" = "Adicionar participantes";
"meeting_schedule_cancel_dialog_message" = "Você quer cancelar a reunião e enviar uma notificação para todos os participantes?";
"meeting_schedule_cancel_dialog_title" = "Cancelar a reunião?";
"meeting_schedule_description_hint" = "Adicionar descrição";
"meeting_schedule_description_title" = "Descrição";
"meeting_waiting_room_join" = "Entrar";
"meeting_waiting_room_joining_title" = "Conexão em andamento";
"meetings_list_no_meeting_for_today" = "Nenhuma reunião agendada para hoje";
"menu_add_address_to_contacts" = "Adicionar aos contatos";
"menu_copy_chat_message" = "Copiar";
"menu_copy_phone_number" = "Copiar número de telefone";
"menu_delete_history" = "Excluir histórico";
"menu_delete_selected_item" = "Excluir";
"menu_forward_chat_message" = "Encaminhar";
"menu_invite" = "Convidar";
"menu_reply_to_chat_message" = "Responder";
"password" = "Senha";
"settings_advanced_device_id_hint" = "Apenas caracteres alfanuméricos";
"settings_advanced_download_apply_remote_provisioning" = "Baixar e aplicar";
"settings_advanced_input_audio_device_title" = "Dispositivo de entrada de áudio padrão";
"settings_contacts_carddav_name_title" = "Nome de exibição";
"settings_contacts_carddav_password_title" = "Senha";
"settings_contacts_carddav_server_url_title" = "URL do servidor";
"settings_contacts_carddav_sync_error_toast" = "Erro na sincronização!";
"settings_contacts_carddav_username_title" = "Nome de usuário";
"settings_contacts_edit_carddav_server_title" = "Editar agenda CardDAV";
"settings_contacts_edit_ldap_server_title" = "Editar servidor LDAP";
"settings_contacts_ldap_bind_dn_title" = "Bind DN";
"settings_contacts_ldap_password_title" = "Senha";
"settings_meetings_layout_mosaic_label" = "Mosaico";
"welcome_page_2_title" = "Seguro";
"welcome_page_title" = "Bem vindo";
"account_settings_audio_video_conference_factory_uri_title" = "URI da fábrica de conferências de áudio/vídeo";
"account_settings_avpf_title" = "AVPF";
"account_settings_bundle_mode_title" = "Modo bundle";
"account_settings_ccmp_server_url_title" = "URL do servidor CCMP";
"account_settings_conference_factory_uri_title" = "URI da fábrica de conferências";
"account_settings_cpim_in_basic_conversations_title" = "Usar CPIM em conversas básicas";
"account_settings_dialog_invalid_password_title" = "Autenticação necessária";
"account_settings_dialog_invalid_password_hint" = "Senha";
"account_settings_enable_ice_title" = "Ativar ICE";
"account_settings_enable_turn_title" = "Ativar TURN";
"account_settings_expire_title" = "Expira (em segundos)";
"account_settings_im_encryption_mandatory_title" = "Criptografia de IM obrigatória";
"account_settings_lime_server_url_title" = "URL do servidor de chaves de criptografia E2E";
"account_settings_mwi_uri_title" = "URI do servidor MWI (Indicador de Mensagem em Espera)";
"account_settings_nat_policy_title" = "Configurações da política NAT";
"account_settings_outbound_proxy_title" = "Proxy de saída";
"account_settings_push_notification_not_available_title" = "As notificações push não estão disponíveis!";
"account_settings_push_notification_title" = "Permitir notificações push";
"account_settings_sip_proxy_url_title" = "URL do servidor proxy SIP";
"account_settings_stun_server_url_title" = "URL do servidor STUN/TURN";
"account_settings_title" = "Configurações da conta";
"account_settings_turn_password_title" = "Senha TURN";
"account_settings_turn_username_title" = "Nome de usuário TURN";
"account_settings_update_password_title" = "Atualizar senha";
"account_settings_voicemail_uri_title" = "URI do correio de voz";
"assistant_already_have_an_account" = "Já tem uma conta?";
"assistant_create_account_using_email_on_our_web_platform" = "Crie uma conta com seu e-mail em:";
"assistant_dialog_confirm_phone_number_title" = "Confirmar número de telefone";
"assistant_dialog_general_terms_and_privacy_policy_title" = "Termos gerais e política de privacidade";
"assistant_dialog_general_terms_label" = "termos gerais";
"assistant_dialog_privacy_policy_label" = "política de privacidade";
"assistant_login_third_party_sip_account" = "Usar uma conta SIP de terceiros";
"assistant_no_account_yet" = "Ainda não tem uma conta?";
"assistant_permissions_grant_all_of_them" = "OK";
"assistant_permissions_skip_permissions" = "Fazer isso mais tarde";
"assistant_permissions_title" = "Conceder permissões";
"assistant_qr_code_invalid_toast" = "QR code inválido!";
"assistant_scan_qr_code" = "Escanear QR code";
"assistant_sip_account_transport_protocol" = "Transporte";
"assistant_third_party_sip_account_warning_ok" = "Eu entendo";
"authentication_id" = "ID de autenticação (se diferente)";
"bottom_navigation_calls_label" = "Chamadas";
"bottom_navigation_contacts_label" = "Contatos";
"bottom_navigation_conversations_label" = "Conversas";
"bottom_navigation_meetings_label" = "Reuniões";
"call_action_blind_transfer" = "Transferir";
"call_action_change_layout" = "Layout";
"call_action_go_to_calls_list" = "Lista de chamadas";
"call_action_hang_up" = "Desligar";
"call_action_pause_call" = "Pausar";
"call_action_record_call" = "Gravar";
"call_action_resume_call" = "Retomar";
"call_action_show_dialer" = "Teclado";
"call_action_show_messages" = "Mensagens";
"call_action_start_new_call" = "Nova chamada";
"call_audio_device_type_earpiece" = "Fone de ouvido (auricular)";
"call_audio_device_type_headphones" = "Fones de ouvido";
"call_audio_device_type_speaker" = "Alto-falante";
"call_audio_incoming" = "Chamada recebida";
"call_dialog_zrtp_security_alert_message" = "A confidencialidade desta chamada pode estar comprometida!";
"call_dialog_zrtp_security_alert_title" = "Alerta de segurança";
"call_dialog_zrtp_security_alert_try_again" = "Tente novamente";
"call_dialog_zrtp_validate_trust_letters_do_not_match" = "Nada corresponde";
"call_dialog_zrtp_validate_trust_local_code_label" = "Seu código:";
"call_dialog_zrtp_validate_trust_remote_code_label" = "Código do correspondente:";
"call_dialog_zrtp_validate_trust_title" = "Valide o dispositivo";
"call_do_zrtp_sas_validation_again" = "Validar ZRTP SAS novamente";
"call_history_deleted_toast" = "O histórico foi excluído";
"call_not_encrypted" = "A chamada não está criptografada";
"call_outgoing" = "Chamada efetuada";
"call_srtp_point_to_point_encrypted" = "Criptografada ponto a ponto por SRTP";
"call_state_connected" = "Ativa";
"call_state_paused" = "Pausada";
"call_state_paused_by_remote" = "Pausada pelo remoto";
"call_state_resuming" = "Retomando…";
"call_stats_audio_title" = "Áudio";
"call_stats_media_encryption_title" = "Criptografia de mídia";
"call_stats_video_title" = "Vídeo";
"call_transfer_failed_toast" = "Falha na transferência da chamada!";
"call_transfer_in_progress_toast" = "A chamada está sendo transferida";
"call_transfer_successful_toast" = "A chamada foi transferida com sucesso";
"call_waiting_for_encryption_info" = "Aguardando criptografia…";
"call_zrtp_end_to_end_encrypted" = "Criptografada de ponta a ponta por ZRTP";
"call_zrtp_sas_validation_required" = "Validação necessária";
"calls_list_dialog_merge_into_conference_label" = "Criar conferência";
"calls_list_dialog_merge_into_conference_title" = "Mesclar todas as chamadas em conferência?";
"conference_layout_audio_only" = "Somente áudio";
"conference_layout_grid" = "Mosaico";
"conference_participant_joining_text" = "Entrando…";
"conference_participant_paused_text" = "Pausado";
"calls_list_title" = "Lista de chamadas";
"conference_action_screen_sharing" = "Compartilhar tela";
"conference_action_show_participants" = "Participantes";
"conference_call_empty" = "Aguardando outros participantes…";
"conference_failed_to_create_group_call_toast" = "Falha ao criar chamada em grupo!";
"conference_layout_active_speaker" = "Alto-falante";
"conference_share_link_title" = "Compartilhar convite";
"connection_error_for_non_default_account" = "Erro de conexão da(s) conta(s)";
"contact_call_action" = "Chamar";
"contact_details_actions_title" = "Outras ações";
"contact_details_add_to_favourites" = "Adicionar aos favoritos";
"contact_details_delete" = "Excluir";
"contact_details_edit" = "Editar";
"contact_details_remove_from_favourites" = "Remover dos favoritos";
"contact_details_share" = "Compartilhar";
"contact_dialog_delete_message" = "Este contato será removido definitivamente.";
"contact_dialog_pick_phone_number_or_sip_address_title" = "Escolha um número ou um endereço SIP";
"contact_edit_title" = "Editar contato";
"contact_editor_company" = "Empresa";
"contact_editor_dialog_abort_confirmation_message" = "Todas as alterações serão perdidas";
"contact_editor_dialog_abort_confirmation_title" = "Não salvar alterações?";
"contact_editor_first_name" = "Nome";
"contact_editor_job_title" = "Cargo";
"contact_editor_last_name" = "Sobrenome";
"contact_message_action" = "Mensagem";
"contact_new_title" = "Novo contato";
"contacts_list_all_contacts_title" = "Todos os contatos";
"contacts_list_empty" = "Nenhum contato no momento…";
"contacts_list_favourites_title" = "Favoritos";
"contacts_list_filter_popup_see_all" = "Ver todos";
"conversation_action_call" = "Chamar";
"conversation_action_configure_ephemeral_messages" = "Configurar mensagens efêmeras";
"conversation_action_delete" = "Excluir conversa";
"conversation_action_leave_group" = "Sair do grupo";
"conversation_action_mark_as_read" = "Marcar como lida";
"conversation_action_mute" = "Silenciar";
"conversation_action_unmute" = "Reativar som";
"conversation_add_participants_title" = "Adicionar participantes";
"conversation_dialog_edit_subject" = "Editar assunto da conversa";
"conversation_dialog_set_subject" = "Definir assunto da conversa";
"conversation_dialog_subject_hint" = "Assunto da conversa";
"conversation_end_to_end_encrypted_event_title" = "Conversa com criptografia de ponta a ponta";
"conversation_end_to_end_encrypted_event_subtitle" = "As mensagens nesta conversa são criptografadas de ponta a ponta. Apenas seu correspondente pode descriptografá-las.";
"conversation_ephemeral_messages_duration_one_day" = "1 dia";
"conversation_ephemeral_messages_duration_one_hour" = "1 hora";
"conversation_ephemeral_messages_duration_one_minute" = "1 minuto";
"conversation_ephemeral_messages_duration_one_week" = "1 semana";
"conversation_ephemeral_messages_duration_three_days" = "3 dias";
"conversation_ephemeral_messages_subtitle" = "Novas mensagens serão excluídas automaticamente assim que lidas por todos.\nEscolha uma duração:";
"conversation_ephemeral_messages_title" = "Mensagens efêmeras";
"conversation_event_conference_created" = "Você entrou no grupo";
"conversation_event_conference_destroyed" = "Você saiu do grupo";
"conversation_event_ephemeral_messages_disabled" = "Mensagens efêmeras foram desativadas";
"conversation_event_ephemeral_messages_enabled" = "Mensagens efêmeras foram ativadas";
"conversation_failed_to_create_toast" = "Falha ao criar conversa!";
"conversation_forward_message_title" = "Encaminhar mensagem para…";
"conversation_info_add_participants_label" = "Adicionar participantes";
"conversation_info_admin_menu_remove_participant" = "Remover do grupo";
"conversation_info_admin_menu_set_participant_admin" = "Dar direitos de admin";
"conversation_info_admin_menu_unset_participant_admin" = "Remover direitos de admin";
"conversation_info_confirm_start_group_call_dialog_message" = "Todos os participantes receberão uma chamada.";
"conversation_info_confirm_start_group_call_dialog_title" = "Iniciar uma chamada em grupo?";
"conversation_info_delete_history_action" = "Excluir histórico";
"conversation_info_menu_add_to_contacts" = "Adicionar aos contatos";
"conversation_message_forwarded_toast" = "A mensagem foi encaminhada";
"conversation_info_menu_go_to_contact" = "Ver perfil do contato";
"conversation_info_participant_is_admin_label" = "Admin";
"conversation_message_meeting_updated_label" = "A reunião foi atualizada";
"conversation_text_field_hint" = "Diga algo…";
"conversation_take_picture_label" = "Tirar foto";
"conversation_pick_file_from_gallery_label" = "Abrir galeria";
"conversation_pick_any_file_label" = "Escolher arquivo";
"conversation_file_cant_be_opened_error_toast" = "O arquivo não pode ser aberto!";
"dialog_no" = "Não";
"dialog_ok" = "OK";
"dialog_yes" = "Sim";
"drawer_menu_account_connection_status_cleared" = "Desativado";
"drawer_menu_account_connection_status_connected" = "Conectado";
"drawer_menu_account_connection_status_failed" = "Erro";
"drawer_menu_account_connection_status_progress" = "Conectando…";
"drawer_menu_add_account" = "Adicionar uma conta";
"drawer_menu_manage_account" = "Gerenciar o perfil";
"drawer_menu_no_account_configured_yet" = "Nenhuma conta configurada ainda";
"generic_address_picker_suggestions_list_title" = "Sugestões";
"help_about_advanced_title" = "Avançado";
"help_about_check_for_update" = "Verificar atualização";
"help_about_privacy_policy_title" = "Política de privacidade";
"help_about_user_guide_subtitle" = "Aprenda a dominar todos os recursos do aplicativo, passo a passo.";
"help_about_version_title" = "Versão";
"help_dialog_update_available_title" = "Atualização disponível";
"help_error_checking_version_toast_message" = "Ocorreu um erro ao verificar por atualizações";
"help_quit_title" = "Sair do aplicativo";
"help_title" = "Ajuda";
"help_troubleshooting_app_version_title" = "Versão do aplicativo";
"manage_account_add_picture" = "Adicionar uma foto";
"manage_account_delete" = "Sair";
"manage_account_details_title" = "Detalhes";
"manage_account_device_last_connection" = "Última conexão:";
"manage_account_device_remove" = "Remover";
"manage_account_devices_title" = "Dispositivos";
"manage_account_dialog_remove_account_message" = "Se você deseja excluir sua conta permanentemente, acesse: https://sip.linphone.org";
"manage_account_dialog_remove_account_title" = "Sair da sua conta?";
"manage_account_edit_picture" = "Editar foto";
"manage_account_international_prefix" = "Prefixo internacional";
"manage_account_status_progress_summary" = "A conta está se conectando ao servidor, por favor, aguarde…";
"manage_account_title" = "Gerenciar conta";
"meeting_failed_to_schedule_toast" = "Falha ao agendar reunião!";
"meeting_info_deleted_toast" = "A reunião foi excluída";
"meeting_info_export_as_calendar_event" = "Criar evento na agenda";
"meeting_info_join_title" = "Entrar na reunião agora";
"meeting_schedule_edit_title" = "Editar reunião";
"meeting_schedule_meeting_label" = "Reunião";
"meeting_schedule_pick_end_time_title" = "Escolha a hora de término";
"meeting_schedule_pick_start_date_title" = "Escolha a data de início";
"meeting_schedule_pick_start_time_title" = "Escolha a hora de início";
"meeting_schedule_send_invitations_title" = "Enviar convite aos participantes";
"meeting_schedule_subject_hint" = "Adicionar título…";
"meeting_schedule_timezone_title" = "Fuso horário";
"meeting_schedule_title" = "Nova reunião";
"meeting_waiting_room_cancel" = "Cancelar";
"menu_resend_chat_message" = "Reenviar";
"or" = "ou";
"phone_number" = "Número de telefone";
"menu_see_existing_contact" = "Ver contato";
"menu_show_imdn" = "Status de entrega";
"message_delivery_info_error_title" = "Erro";
"message_forwarded_label" = "Encaminhada";
"message_reaction_click_to_remove_label" = "Clique para remover";
"network_not_reachable" = "Você não está conectado à internet";
"new_conversation_create_group" = "Criar uma conversa em grupo";
"new_conversation_search_bar_filter_hint" = "Pesquisar contato";
"new_conversation_title" = "Nova conversa";
"next" = "Próximo";
"notification_missed_call_title" = "Chamada perdida";
"operation_in_progress_overlay" = "Operação em andamento, por favor, aguarde";
"recordings_title" = "Gravações";
"settings_advanced_accept_early_media_title" = "Aceitar mídia antecipada";
"settings_advanced_allow_outgoing_early_media_title" = "Permitir mídia antecipada de saída";
"settings_advanced_audio_codecs_title" = "Codecs de áudio";
"settings_advanced_audio_devices_title" = "Dispositivos de áudio";
"settings_advanced_device_id" = "ID do dispositivo";
"settings_advanced_media_encryption_mandatory_title" = "Criptografia de mídia obrigatória";
"settings_advanced_output_audio_device_title" = "Dispositivo de saída de áudio padrão";
"settings_advanced_remote_provisioning_url" = "URL de provisionamento remoto";
"settings_advanced_title" = "Configurações avançadas";
"settings_advanced_upload_server_url" = "URL do servidor de compartilhamento de arquivos";
"settings_advanced_video_codecs_title" = "Codecs de vídeo";
"settings_calls_adaptive_rate_control_title" = "Controle adaptativo de taxa";
"settings_calls_auto_record_title" = "Iniciar gravação de chamadas automaticamente";
"settings_calls_calibrate_echo_canceller_done_no_echo" = "sem eco";
"settings_calls_calibrate_echo_canceller_failed" = "falhou";
"settings_calls_calibrate_echo_canceller_in_progress" = "em andamento";
"settings_calls_calibrate_echo_canceller_title" = "Calibrar cancelador de eco";
"settings_calls_change_ringtone_title" = "Alterar toque";
"settings_calls_echo_canceller_subtitle" = "Impede que o eco seja ouvido pela outra parte se não houver um cancelador de eco de hardware disponível";
"settings_calls_echo_canceller_title" = "Usar cancelador de eco por software";
"settings_calls_enable_fec_title" = "Ativar FEC de vídeo";
"settings_calls_enable_video_title" = "Ativar vídeo";
"settings_calls_title" = "Chamadas";
"settings_calls_vibrate_while_ringing_title" = "Vibrar enquanto a chamada recebida está tocando";
"settings_contacts_add_carddav_server_title" = "Adicionar agenda CardDAV";
"settings_contacts_add_ldap_server_title" = "Adicionar servidor LDAP";
"settings_contacts_ldap_search_base_title" = "Base de busca (não pode ficar em branco)";
"settings_contacts_ldap_search_filter_title" = "Filtro";
"settings_contacts_ldap_server_url_title" = "URL do servidor (não pode ficar em branco)";
"settings_contacts_ldap_use_tls_title" = "Usar TLS";
"settings_contacts_title" = "Contatos";
"settings_conversations_auto_download_title" = "Baixar arquivos automaticamente";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Marcar conversa como lida ao dispensar notificação de mensagem";
"settings_conversations_title" = "Conversas";
"settings_meetings_default_layout_title" = "Layout padrão";
"settings_meetings_layout_active_speaker_label" = "Orador ativo";
"settings_meetings_title" = "Reuniões";
"settings_network_allow_ipv6" = "Permitir IPv6";
"settings_network_title" = "Rede";
"settings_network_use_wifi_only" = "Usar apenas redes Wi-Fi";
"settings_security_enable_vfs_subtitle" = "Aviso: uma vez ativado, não pode ser desativado!";
"settings_security_enable_vfs_title" = "Criptografar tudo";
"settings_security_prevent_screenshots_title" = "Impedir que a interface seja gravada";
"settings_security_title" = "Segurança";
"settings_title" = "Configurações";
"sip_address_copied_to_clipboard_toast" = "Endereço SIP copiado para a área de transferência";
"sip_address_display_name" = "Nome de exibição";
"sip_address_domain" = "Domínio";
"start" = "Início";
"uri_handler_config_success_toast" = "Configuração aplicada com sucesso";
"username" = "Nome de usuário";
"welcome_page_3_title" = "Código aberto";
"[linphone.org/contact](https://linphone.org/contact)" = "[linphone.org/contact](https://linphone.org/contact)";
"*" = "*";
"**%@**" = "**%@**";
"#" = "#";
"%@" = "%@";
"%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";
"account_settings_dialog_invalid_password_message" = "A conexão falhou porque a autenticação está ausente ou é inválida para a conta \n%@.\n\nVocê pode fornecer a senha novamente ou verificar a configuração da sua conta nas configurações.";
"assistant_account_creation_sms_confirmation_explanation" = "Enviamos um código de verificação para o seu número de telefone %@. Por favor, insira o código de verificação abaixo:";
"assistant_dialog_confirm_phone_number_message" = "Tem certeza de que deseja usar o número de telefone %@?";
"assistant_permissions_subtitle" = "Para aproveitar ao máximo o %@, precisamos que você nos conceda as seguintes permissões:";
"assistant_dialog_general_terms_and_privacy_policy_message" = "Ao continuar, você aceita nossos %@ e %@.";
"assistant_forgotten_password" = "Esqueceu a senha?";
"assistant_invalid_uri_toast" = "URI inválido";
"assistant_permissions_access_camera_title" = "**Acessar a câmera:** Para capturar vídeo durante videochamadas e conferências.";
"assistant_permissions_post_notifications_title" = "**Publicar notificações:** Para ser informado quando você receber uma mensagem ou uma chamada.";
"assistant_permissions_read_contacts_title" = "**Ler contatos:** Para exibir seus contatos e encontrar quem está usando o %@.";
"assistant_third_party_sip_account_create_linphone_account" = "Prefiro criar uma conta";
"assistant_web_platform_link" = "subscribe.linphone.org";
"call_action_attended_transfer" = "Transferência assistida";
"call_audio_device_type_bluetooth" = "Bluetooth (%@)";
"assistant_permissions_record_audio_title" = "**Gravar áudio:** Para que seu correspondente possa ouvi-lo e para gravar mensagens de voz.";
"assistant_third_party_sip_account_warning_explanation" = "Alguns recursos exigem uma conta %@, como mensagens em grupo, videoconferências…\n\nEsses recursos ficam ocultos quando você se registra com uma conta SIP de terceiros.\n\nPara habilitá-los em um projeto comercial, entre em contato conosco.";
"call_can_be_trusted_toast" = "Dispositivo autenticado";
"call_transfer_current_call_title" = "Transferir chamada";
"call_zrtp_sas_validation_skip" = "Pular";
"calls_count_label" = "%@ chamadas";
"Ce mode vous permet dêtre interopérable avec dautres services SIP.\nVos communications seront chiffrées de point à point. " = "Este modo permite que você seja interoperável com outros serviços SIP.\nSuas comunicações serão criptografadas de ponto a ponto. ";
"Chiffrement de bout en bout de tous vos échanges, grâce au mode default vos communications sont à labri des regards." = "Criptografia de ponta a ponta de todas as suas trocas, graças ao modo padrão, suas comunicações ficam protegidas de olhares indiscretos.";
"call_dialog_zrtp_validate_trust_warning_message" = "Para sua segurança, precisamos reautenticar o dispositivo do seu correspondente. Por favor, troquem novamente seus códigos:";
"call_dialog_zrtp_validate_trust_message" = "Para sua segurança, precisamos autenticar o dispositivo do seu correspondente. Por favor, troquem seus códigos:";
"conference_name_error" = "Erro no nome da conferência";
"contact_dialog_delete_title" = "Excluir %@?";
"contact_video_call_action" = "Chamada de vídeo";
"contacts_list_filter_popup_see_linphone_only" = "Ver contatos %@";
"conversation_composing_label_multiple" = "%@ estão digitando…";
"conversation_composing_label_single" = "%@ está digitando…";
"conversation_end_to_end_encrypted_bottom_sheet_link" = "https://linphone.org/en/features/#security";
"contact_details_numbers_and_addresses_title" = "Números de telefone e endereços SIP";
"conversation_ephemeral_messages_duration_multiple_days" = "%d dias";
"conversation_event_admin_set" = "%@ é admin";
"conversation_event_admin_unset" = "%@ não é mais admin";
"conversation_event_device_removed" = "Dispositivo de %@ removido";
"conversation_event_device_added" = "Novo dispositivo para %@";
"conversation_event_ephemeral_messages_lifetime_changed" = "A duração efêmera agora é %@";
"conversation_event_participant_added" = "%@ entrou";
"conversation_info_participants_list_title" = "Membros do grupo (%d)";
"conversation_message_meeting_cancelled_label" = "A reunião foi cancelada!";
"conversation_one_to_one_hidden_subject" = "Dummy subject";
"conversation_reply_to_message_title" = "Respondendo a: ";
"debug_logs_copied_to_clipboard_toast" = "Logs de depuração copiados para a área de transferência";
"Default" = "Padrão";
"Default mode" = "Modo padrão";
"dialog_close" = "Fechar";
"help_about_title" = "Sobre o Linphone";
"drawer_menu_account_connection_status_refreshing" = "Atualizando ...";
"DTLS" = "DTLS";
"failed_meeting_ics_invitation_not_sent_toast" = "Não foi possível enviar convites ICS da reunião para nenhum participante";
"GC_MSG" = "Você foi adicionado a uma sala de chat";
"help_about_contribute_translations_title" = "Contribua na tradução do Linphone";
"help_about_open_source_licenses_subtitle" = "© Belledonne Communications 2010-2024";
"help_about_open_source_licenses_title" = "GNU General Public License v3.0";
"help_about_privacy_policy_subtitle" = "Quais informações o Linphone coleta e usa";
"help_about_user_guide_title" = "Guia do usuário Linphone";
"help_dialog_update_available_message" = "Uma nova versão %@ está disponível. Você quer atualizar?";
"history_list_empty_with_filter_history" = "Nenhum registro corresponde à sua pesquisa";
"history_title" = "Histórico de chamadas";
"Interoperable" = "Interoperável";
"IM_MSG" = "Você recebeu uma mensagem";
"manage_account_dialog_international_prefix_help_message" = "Escolha seu país para permitir que o Linphone corresponda aos seus contatos.";
"meeting_call_remove_no_participants" = "Nenhum participante no momento…";
"meeting_call_remove_participant_confirmation_message" = "Tem certeza de que deseja remover %@ ?";
"meeting_call_remove_participant_confirmation_title" = "Remover um participante";
"meeting_exported_as_calendar_event" = "Reunião adicionada à agenda do iPhone";
"meeting_failed_to_edit_toast" = "Falha ao editar reunião";
"meeting_schedule_failed_no_subject_or_participant_toast" = "Um assunto e pelo menos um participante são necessários para criar uma reunião";
"meeting_waiting_room_joining_subtitle" = "Você entrará em breve";
"meetings_list_empty" = "Nenhuma reunião no momento…";
"menu_block_address" = "Bloquear o endereço";
"menu_block_number" = "Bloquear o número";
"menu_copy_sip_address" = "Copiar endereço SIP";
"message_copied_to_clipboard_toast" = "Mensagem copiada para a área de transferência";
"message_delivery_info_read_title" = "Lida";
"message_delivery_info_received_title" = "Recebida";
"message_delivery_info_sent_title" = "Enviada";
"message_meeting_invitation_cancelled_notification" = "📅 A reunião foi cancelada";
"message_meeting_invitation_notification" = "📅 Você foi convidado para uma reunião";
"message_meeting_invitation_updated_notification" = "📅 A reunião foi atualizada";
"message_reactions_info_all_title" = "Reações";
"network_reachable_again" = "A rede está acessível novamente";
"None" = "Nenhum";
"notification_chat_message_reaction_received" = "%@ reagiu com %@ para: %@";
"Personnalize your profil mode" = "Personalize o modo do seu perfil";
"picker_categories" = "Categorias";
"selected_participants_count" = "%@ participantes selecionados";
"qr_code_validated" = "QR code validado";
"settings_calls_calibrate_echo_canceller_done" = "%@ ms";
"settings_contacts_carddav_deleted_toast" = "Conta CardDAV excluída";
"settings_contacts_carddav_mandatory_field_not_filled_toast" = "Por favor, preencha pelo menos o nome de exibição e a URL do servidor";
"settings_contacts_carddav_sync_successful_toast" = "Sincronização bem-sucedida";
"settings_contacts_carddav_use_as_default_title" = "Armazenar contatos recém-criados aqui";
"settings_contacts_carddav_realm_title" = "Domínio de autenticação";
"settings_contacts_ldap_bind_user_password_title" = "Senha do usuário de autenticação";
"settings_contacts_ldap_max_results_title" = "Máximo de resultados";
"settings_contacts_ldap_request_timeout_title" = "Tempo limite da solicitação";
"sip_address" = "Endereço SIP";
"sip.linphone.org" = "sip.linphone.org";
"SRTP" = "SRTP";
"TCP" = "TCP";
"Temp Help" = "Ajuda Temp";
"text_copied_to_clipboard_toast" = "Texto copiado para a área de transferência";
"username_error" = "Erro no nome de usuário";
"web_platform_forgotten_password_url" = "https://subscribe.linphone.org/";
"website_contact_url" = "https://linphone.org/contact";
"website_download_url" = "https://linphone.org/linphone-softphone";
"website_user_guide_url" = "https://linphone.org/en/docs/";
"welcome_page_subtitle" = "ao %@";
"ZRTP" = "ZRTP";
"conversation_event_participant_removed" = "%@ saiu";
"conversation_event_subject_changed" = "Novo assunto: %@";
"conversations_files_waiting_to_be_shared_single" = "1 arquivo esperando para ser compartilhado";
"conversations_files_waiting_to_be_shared_multiple" = "%@ arquivos esperando para ser compartilhados";
"notification_chat_message_received_title" = "Mensagem recebida";
"TLS" = "TLS";
"UDP" = "UDP";
"uri_handler_bad_call_address_failed_toast" = "Não é possível ligar, endereço inválido";
"uri_handler_bad_config_address_failed_toast" = "Não é possível recuperar a configuração, endereço inválido";
"uri_handler_call_failed_toast" = "Falha na chamada";
"uri_handler_config_failed_toast" = "Falha na configuração";
"web_platform_register_email_url" = "https://subscribe.linphone.org/register/email";
"website_open_source_licences_usage_url" = "https://wiki.linphone.org/xwiki/wiki/public/view/Linphone/Third%20party%20components%20/";
"website_privacy_policy_url" = "https://linphone.org/en/privacy-policy";
"website_terms_and_conditions_url" = "https://www.linphone.org/en/terms-of-use";
"website_translate_weblate_url" = "https://weblate.linphone.org/";
"[https://sip.linphone.org](https://sip.linphone.org)" = "[https://sip.linphone.org](https://sip.linphone.org)";
": %@" = ": %@";
"welcome_carousel_skip" = "Pular";
"welcome_page_1_message" = "Um aplicativo de comunicação **seguro**, de **código aberto** e **francês**.";
"welcome_page_2_message" = "Suas comunicações estão seguras graças à nossa **criptografia de ponta a ponta**.";
"welcome_page_3_message" = "Um aplicativo **gratuito** e de código aberto desde **2001**.";
"You will change this mode later" = "Você mudará este modo mais tarde";

View file

@ -0,0 +1,8 @@
"[linphone.org/contact](https://linphone.org/contact)" = "[linphone.org/contact](https://linphone.org/contact)";
"*" = "*";
"**%@**" = "**%@**";
"#" = "#";
"%@" = "%@";
"%lld" = "%lld";
"[https://sip.linphone.org](https://sip.linphone.org)" = "[https://sip.linphone.org](https://sip.linphone.org)";
": %@" = ": %@";

View file

@ -187,7 +187,7 @@
"dialog_deny" = "Отказать";
"dialog_install" = "Установить";
"dialog_no" = "Нет";
"dialog_ok" = "ОК";
"dialog_confirm" = "ОК";
"dialog_yes" = "Да";
"drawer_menu_account_connection_status_cleared" = "Отключить";
"drawer_menu_account_connection_status_connected" = "Подключен";
@ -362,3 +362,4 @@
"username" = "Имя пользователя";
"conversation_end_to_end_encrypted_event_title" = "Сквозное шифрование беседы";
"conversation_end_to_end_encrypted_event_subtitle" = "Сообщения в этой беседе зашифрованы end-to-end шифрованием. Расшифровать их может только ваш собеседник.";
"authentication_id" = "Идентификатор аутентификации (если отличается)";

View file

@ -0,0 +1,382 @@
"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" = "Inštalovať";
"dialog_no" = "Nie";
"dialog_confirm" = "Confirm";
"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)";
"conversation_ephemeral_messages_subtitle" = "Nové správy sa automaticky odstránia, keď si ich všetci prečítajú.\nVyberte dobu trvania:";
"conversation_ephemeral_messages_duration_one_minute" = "1 minúta";
"conversation_ephemeral_messages_duration_one_hour" = "1 hodina";
"conversation_ephemeral_messages_duration_one_day" = "1 deň";
"conversation_ephemeral_messages_duration_three_days" = "3 dni";
"conversation_ephemeral_messages_duration_one_week" = "1 týždeň";
"conversation_add_participants_title" = "Pridať účastníkov";
"conversation_end_to_end_encrypted_event_title" = "Koncové šifrovanie konverzácie";
"conversation_end_to_end_encrypted_event_subtitle" = "Správy v tejto konverzácii sú koncovo šifrované. Dešifrovať ich môže len váš partner v konverzácii.";
"conversation_dialog_set_subject" = "Nastaviť predmet konverzácie";
"conversation_dialog_edit_subject" = "Upraviť predmet konverzácie";
"conversation_dialog_subject_hint" = "Predmet konverzácie";
"conversation_event_ephemeral_messages_enabled" = "Dočasné (miznúce) správy boli povolené";
"conversation_event_ephemeral_messages_disabled" = "Dočasné (miznúce) správy boli zakázané";
"conversation_event_conference_created" = "Pridali ste sa ku skupine";
"conversation_event_conference_destroyed" = "Opustili ste skupinu";
"conversation_info_add_participants_label" = "Pridať účastníkov";
"conversation_info_delete_history_action" = "Vymazať históriu";
"conversation_info_admin_menu_remove_participant" = "Odstrániť zo skupiny";
"conversation_info_admin_menu_set_participant_admin" = "Udeliť práva administrátora";
"conversation_info_admin_menu_unset_participant_admin" = "Odobrať práva administrátora";
"conversation_info_menu_go_to_contact" = "Zobraziť profil kontaktu";
"conversation_info_menu_add_to_contacts" = "Pridať do kontaktov";
"conversation_info_confirm_start_group_call_dialog_message" = "Bude volané všetkým účastníkom.";
"conversation_text_field_hint" = "Napíšte niečo…";
"conversation_menu_go_to_info" = "Informácia o konverzácii";
"conversation_invalid_participant_due_to_security_mode_toast" = "Vytvorenie konverzácie s účastníkom z inej domény nie je z bezpečnostných dôvodov povolené!";
"conversation_take_picture_label" = "Odfotiť";
"conversation_pick_file_from_gallery_label" = "Otvoriť galériu";
"conversation_pick_any_file_label" = "Vybrať súbor";
"conversation_file_cant_be_opened_error_toast" = "Súbor sa nedá otvoriť!";
"new_conversation_title" = "Nová konverzácia";
"new_conversation_search_bar_filter_hint" = "Hľadať kontakt";
"new_conversation_create_group" = "Vytvoriť skupinovú konverzáciu";
"conversation_info_participant_is_admin_label" = "Administrátor";
"conversation_info_confirm_start_group_call_dialog_title" = "Zahájiť skupinový hovor?";
"conversation_failed_to_create_toast" = "Vytvorenie konverzácie zlyhalo!";
"call_action_go_to_calls_list" = "Zoznam hovorov";
"call_action_change_layout" = "Rozloženie";
"call_outgoing" = "Odchádzajúci hovor";
"call_audio_incoming" = "Prichádzajúci hovor";
"call_action_show_messages" = "Správy";
"call_action_pause_call" = "Pozastaviť";
"call_action_resume_call" = "Obnoviť";
"call_action_record_call" = "Nahrať";
"call_action_hang_up" = "Zavesiť";
"call_do_zrtp_sas_validation_again" = "Znovu overiť ZRTP SAS";
"call_not_encrypted" = "Hovor nie je šifrovaný";
"call_dialog_zrtp_validate_trust_local_code_label" = "Váš kód:";
"call_dialog_zrtp_validate_trust_remote_code_label" = "Kód protistrany:";
"call_dialog_zrtp_validate_trust_letters_do_not_match" = "Žiadna zhoda";
"call_dialog_zrtp_security_alert_title" = "Bezpečnostné upozornenie";
"call_dialog_zrtp_security_alert_try_again" = "Skúsiť znovu";
"call_dialog_zrtp_security_alert_message" = "Dôvernosť tohto hovoru môže byť ohrozená!";
"call_audio_device_type_earpiece" = "Slúchadlo";
"call_audio_device_type_speaker" = "Reproduktor";
"call_state_connected" = "Aktívny";
"call_state_paused" = "Pozastavené";
"call_state_paused_by_remote" = "Pozastavené vzdialenou stranou";
"call_state_resuming" = "Obnovujem…";
"call_stats_audio_title" = "Zvuk";
"call_zrtp_sas_validation_required" = "Vyžadované overenie";
"calls_list_title" = "Zoznam hovorov";
"calls_list_dialog_merge_into_conference_title" = "Zlúčiť všetky hovory do konferencie?";
"calls_list_dialog_merge_into_conference_label" = "Vytvoriť konferenciu";
"conversation_forward_message_title" = "Preposlať správu…";
"conversation_message_forwarded_toast" = "Správa bola preposlaná";
"conversation_message_forward_cancelled_toast" = "Preposlanie správy bolo zrušené";
"meeting_schedule_meeting_label" = "Schôdzka";
"meeting_schedule_pick_start_date_title" = "Zvoľte dátum začiatku";
"meeting_schedule_pick_start_time_title" = "Zvoľte čas začiatku";
"meeting_schedule_pick_end_time_title" = "Zvoľte čas konca";
"meeting_schedule_description_hint" = "Pridať popis";
"meeting_schedule_add_participants_title" = "Pridať účastníkov";
"meeting_info_join_title" = "Pripojiť sa k schôdzke teraz";
"meeting_info_organizer_label" = "Organizátor";
"meeting_info_export_as_calendar_event" = "Vytvoriť udalosť v kalendári";
"meeting_info_deleted_toast" = "Schôdzka bola vymazaná";
"meeting_schedule_description_title" = "Popis";
"meeting_schedule_edit_title" = "Upraviť schôdzku";
"meeting_schedule_cancel_dialog_title" = "Zrušiť schôdzku?";
"meeting_schedule_cancel_dialog_message" = "Želáte si zrušiť schôdzku a odoslať oznámenie všetkým účastníkom?";
"meeting_info_created_toast" = "Schôdzka bola vytvorená";
"meeting_info_updated_toast" = "Schôdzka bola aktualizovaná";
"meeting_failed_to_schedule_toast" = "Naplánovanie schôdzky zlyhalo!";
"meeting_failed_to_send_invites_toast" = "Odoslanie všetkých pozvánok na schôdzku zlyhalo!";
"meeting_schedule_title" = "Nová schôdzka";
"meeting_schedule_timezone_title" = "Časová zóna";
"meeting_waiting_room_join" = "Pripojiť sa";
"meeting_waiting_room_joining_title" = "Prebieha pripojenie";
"message_reaction_click_to_remove_label" = "Kliknutím odstránite";
"message_forwarded_label" = "Preposlané";
"call_waiting_for_encryption_info" = "Čaká sa na šifrovanie…";
"call_zrtp_end_to_end_encrypted" = "Koncovo šifrované pomocou ZRTP";
"call_dialog_zrtp_validate_trust_title" = "Overiť zariadenie";
"meeting_schedule_send_invitations_title" = "Odoslať pozvánku účastníkom";
"call_audio_device_type_headphones" = "Slúchadlá";
"meeting_schedule_subject_hint" = "Pridať názov…";
"call_action_show_dialer" = "Číselník (numerická klávesnica)";
"call_srtp_point_to_point_encrypted" = "Koncové šifrovanie pomocou SRTP";
"meetings_list_no_meeting_for_today" = "Nadnes nie je naplánovaná žiadna schôdzka";
"meeting_info_cancelled_toast" = "Schôdzka bola zrušená";
"meeting_failed_to_send_part_of_invites_toast" = "Odoslanie pozvánok na schôdzku niektorým účastníkom zlyhalo!";
"account_settings_dialog_invalid_password_message" = "Pripojenie sa nepodarilo, pretože chýba alebo je neplatné overenie účtu\n%@.\n\nPokúste sa znovu zadať heslo alebo skontrolovať konfiguráciu účtu v nastaveniach.";
": %@" = ": %@";
"[https://sip.linphone.org](https://sip.linphone.org)" = "[https://sip.linphone.org](https://sip.linphone.org)";
"[linphone.org/contact](https://linphone.org/contact)" = "[linphone.org/contact](https://linphone.org/contact)";
"*" = "*";
"assistant_dialog_general_terms_and_privacy_policy_message" = "Pokračovaním akceptujete naše %1$s a %2$s.";
"assistant_third_party_sip_account_create_linphone_account" = "Preferujem vytvorenie účtu";
"assistant_permissions_subtitle" = "Pre plné využívanie %@ potrebujeme udelenie nasledovných oprávnení:";
"assistant_account_creation_sms_confirmation_explanation" = "Poslali sme Vám overovací kód na Vaše telefónne číslo %@. Zadajte nižšie, prosím, verifikačný kód:";
"assistant_forgotten_password" = "Zabudnuté heslo?";
"assistant_invalid_uri_toast" = "Neplatná URI adresa";
"call_history_deleted_toast" = "História bola vymazaná";
"call_stats_video_title" = "Video (obraz)";
"call_transfer_in_progress_toast" = "Hovor je presmerovaný";
"call_transfer_failed_toast" = "Presmerovanie hovoru zlyhalo!";
"call_transfer_successful_toast" = "Hovor bol úspešne presmerovaný";
"conference_share_link_title" = "Zdieľať pozvánku";
"conference_call_empty" = "Čakanie na ďalších účastníkov…";
"conference_action_screen_sharing" = "Zdieľať obrazovku";
"conference_action_show_participants" = "Účastníci";
"conference_failed_to_create_group_call_toast" = "Vytvorenie skupinového hovoru zlyhalo!";
"conference_participant_joining_text" = "Pripojovanie…";
"conference_participant_paused_text" = "Pozastavené";
"conference_layout_active_speaker" = "Hovoriaci (rečník)";
"conference_layout_audio_only" = "Iba zvuk";
"connection_error_for_non_default_account" = "Chyba pripojenia účtu(-ov)";
"default_account_disabled" = "Zvolený účet je momentálne zakázaný";
"generic_address_picker_suggestions_list_title" = "Návrhy";
"help_about_title" = "O aplikácii Linphone";
"help_about_contribute_translations_title" = "Prispieť do prekladu Linphone";
"help_about_open_source_licenses_subtitle" = "© Belledonne Communications 2010-2024";
"help_about_open_source_licenses_title" = "GNU General Public License v3.0";
"list_filter_no_result_found" = "Žiadne výsledky…";
"menu_add_address_to_contacts" = "Pridať do kontaktov";
"menu_see_existing_contact" = "Zobraziť kontakt";
"menu_copy_phone_number" = "Kopírovať telefónne číslo";
"menu_delete_history" = "Vymazať históriu";
"menu_invite" = "Pozvať";
"menu_resend_chat_message" = "Znovu odoslať";
"menu_show_imdn" = "Stav doručenia";
"menu_forward_chat_message" = "Preposlať";
"menu_copy_chat_message" = "Kopírovať";
"network_not_reachable" = "Nie ste pripojený k internetu";
"operation_in_progress_overlay" = "Prebieha operácia, prosím, čakajte";
"recordings_title" = "Nahrávky";
"settings_contacts_ldap_max_results_title" = "Maximálny počet výsledkov";
"welcome_page_subtitle" = "v %@";
"call_action_blind_transfer" = "Presmerovať";
"conversation_message_meeting_updated_label" = "Schôdzka bola aktualizovaná";
"meeting_info_delete" = "Vymazať schôdzku";

View file

@ -377,7 +377,7 @@
"conversation_ephemeral_messages_duration_one_week" = "1 тиждень";
"conversation_ephemeral_messages_duration_three_days" = "3 доби";
"conversation_ephemeral_messages_duration_one_day" = "1 доба";
"dialog_ok" = "ОК";
"dialog_confirm" = "ОК";
"welcome_page_title" = "Ласкаво просимо";
": %@" = ": %@";
"[https://sip.linphone.org](https://sip.linphone.org)" = "[https://sip.linphone.org](https://sip.linphone.org)";
@ -456,9 +456,9 @@
"message_delivery_info_read_title" = "Читати";
"message_delivery_info_received_title" = "Отримано";
"message_delivery_info_sent_title" = "Відправлено";
"message_meeting_invitation_cancelled_notification" = "📅 Нараду скасовано";
"message_meeting_invitation_notification" = "📅 Вас запрошено на нараду";
"message_meeting_invitation_updated_notification" = "📅 Нараду оновлено";
"message_meeting_invitation_cancelled_notification" = "Нараду скасовано";
"message_meeting_invitation_notification" = "Вас запрошено на нараду";
"message_meeting_invitation_updated_notification" = "Нараду оновлено";
"message_reactions_info_all_title" = "Реакції";
"network_reachable_again" = "Мережа знову доступна";
"None" = "Жоден";
@ -522,3 +522,4 @@
"conversations_files_waiting_to_be_shared_single" = "1 файл очікує на спільний доступ";
"conversations_files_waiting_to_be_shared_multiple" = "%@ файлів, що очікують на спільний доступ";
"conversation_ephemeral_messages_duration_multiple_days" = "%d днів";
"authentication_id" = "Ідентифікатор автентифікації (якщо відрізняється)";

View file

@ -181,7 +181,7 @@
"dialog_deny" = "拒绝";
"dialog_install" = "安装";
"dialog_no" = "否";
"dialog_ok" = "OK";
"dialog_confirm" = "Confirm";
"dialog_yes" = "是";
"drawer_menu_account_connection_status_cleared" = "禁用";
"drawer_menu_account_connection_status_connected" = "已连接";
@ -362,3 +362,5 @@
"menu_show_imdn" = "送达状态";
"conversation_end_to_end_encrypted_event_title" = "端到端加密聊天";
"conversation_end_to_end_encrypted_event_subtitle" = "此聊天中的消息是端对端e2e加密的。只有您的对话方才能解密它们。";
"authentication_id" = "认证ID";
"settings_contacts_ldap_search_filter_title" = "搜索base不能为空 过滤";

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

@ -20,20 +20,19 @@
import SwiftUI
struct SplashScreen: View {
var body: some View {
GeometryReader { _ in
VStack {
Spacer()
HStack {
Spacer()
Image("linphone")
Spacer()
}
Spacer()
}
ZStack {
Color.white
.ignoresSafeArea()
Image("linphone")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 240, height: 128)
.foregroundColor(ThemeManager.shared.currentTheme.main500)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea(.all)
}
}

View file

@ -195,17 +195,13 @@ class TelecomManager: ObservableObject {
}
}
private func makeRecordFilePath() -> String {
var filePath = "recording_"
let now = Date()
let dateFormat = DateFormatter()
dateFormat.dateFormat = "E-d-MMM-yyyy-HH-mm-ss"
let date = dateFormat.string(from: now)
filePath = filePath.appending("\(date).mkv")
private func makeRecordFilePath(address: String) -> String {
var filePath = "call_recording_sip_" + address.dropFirst(4) + "_on_"
let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
let writablePath = paths[0]
return writablePath.appending("/\(filePath)")
filePath = filePath.appending("\(Int(Date().timeIntervalSince1970)).mkv")
let writablePath = FileUtil.sharedContainerUrl().appendingPathComponent("Library/Recordings/\(filePath)")
return writablePath.path
}
func doCall(core: Core, addr: Address, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws {
@ -237,7 +233,7 @@ class TelecomManager: ObservableObject {
// Log.directLog(BCTBX_LOG_DEBUG, text: "record file path: \(writablePath)")
// lcallParams.recordFile = writablePath
lcallParams.recordFile = makeRecordFilePath()
lcallParams.recordFile = makeRecordFilePath(address: addr.asStringUriOnly())
if isSas {
lcallParams.mediaEncryption = .ZRTP
@ -292,7 +288,7 @@ class TelecomManager: ObservableObject {
func acceptCall(core: Core, call: Call, hasVideo: Bool) {
do {
let callParams = try core.createCallParams(call: call)
callParams.recordFile = makeRecordFilePath()
callParams.recordFile = makeRecordFilePath(address: call.remoteAddress?.asStringUriOnly() ?? "")
callParams.videoEnabled = hasVideo
/*if (ConfigManager.instance().lpConfigBoolForKey(key: "edge_opt_preference")) {
let low_bandwidth = (AppManager.network() == .network_2g)
@ -342,11 +338,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)")
}
}
}
@ -435,6 +433,7 @@ class TelecomManager: ObservableObject {
func onCallStateChanged(core: Core, call: Call, state cstate: Call.State, message: String) {
let callLog = call.callLog
let callId = callLog?.callId ?? ""
if !callInProgress && participantsInvited {
if let remoteAddress = call.remoteAddress {
let uuid = UUID()
@ -623,6 +622,11 @@ class TelecomManager: ObservableObject {
Log.info("CallKit: outgoing call started connecting with uuid \(uuid!) and callId \(callId)")
providerDelegate.reportOutgoingCallStartedConnecting(uuid: uuid!)
} else if callId != "" && cstate == .OutgoingInit {
if let uuidTmp = providerDelegate.uuids["\(callId)"] {
providerDelegate.uuids.removeValue(forKey: callId)
providerDelegate.uuids.updateValue(uuidTmp, forKey: "")
}
} else {
referedToCall = callId
}

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)?
@ -66,25 +70,33 @@ struct LoginFragment: View {
let contentPopup3 = Text(.init(splitMsg[1]))
let contentPopup4 = Text(.init(privacyPolicy)).underline()
let contentPopup5 = Text(.init(splitMsg[2]))
PopupView(isShowPopup: $isShowPopup,
title: Text("assistant_dialog_general_terms_and_privacy_policy_title"),
content: contentPopup1 + contentPopup2 + contentPopup3 + contentPopup4 + contentPopup5,
titleFirstButton: Text("dialog_deny"),
actionFirstButton: {self.isShowPopup.toggle()},
titleSecondButton: Text("dialog_accept"),
actionSecondButton: {acceptGeneralTerms()})
PopupView(
isShowPopup: $isShowPopup,
title: Text("assistant_dialog_general_terms_and_privacy_policy_title"),
content: contentPopup1 + contentPopup2 + contentPopup3 + contentPopup4 + contentPopup5,
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_accept"),
actionSecondButton: { acceptGeneralTerms() },
titleThirdButton: Text("dialog_deny"),
actionThirdButton: { self.isShowPopup.toggle() }
)
.background(.black.opacity(0.65))
.onTapGesture {
self.isShowPopup.toggle()
}
} else { // backup just in case
PopupView(isShowPopup: $isShowPopup,
title: Text("assistant_dialog_general_terms_and_privacy_policy_title"),
content: Text(.init(String(format: String(localized: "assistant_dialog_general_terms_and_privacy_policy_message"), generalTerms, privacyPolicy))),
titleFirstButton: Text("dialog_deny"),
actionFirstButton: {self.isShowPopup.toggle()},
titleSecondButton: Text("dialog_accept"),
actionSecondButton: {acceptGeneralTerms()})
PopupView(
isShowPopup: $isShowPopup,
title: Text("assistant_dialog_general_terms_and_privacy_policy_title"),
content: Text(.init(String(format: String(localized: "assistant_dialog_general_terms_and_privacy_policy_message"), generalTerms, privacyPolicy))),
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_accept"),
actionSecondButton: { acceptGeneralTerms() },
titleThirdButton: Text("dialog_deny"),
actionThirdButton: { self.isShowPopup.toggle() }
)
.background(.black.opacity(0.65))
.onTapGesture {
self.isShowPopup.toggle()
@ -93,6 +105,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 +149,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 +353,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 +387,7 @@ struct LoginFragment: View {
.clipped()
}
.frame(minHeight: geometry.size.height)
.padding(.bottom, keyboard.currentHeight)
}
func acceptGeneralTerms() {

View file

@ -145,20 +145,21 @@ struct ProfileModeFragment: View {
}
if self.isShowPopup {
PopupView(isShowPopup: $isShowPopup,
title: Text(isShowPopupForDefault ? "Default mode" : "Interoperable mode"),
content: Text(
isShowPopupForDefault
? "Texte explicatif du default mode : lorem ipsum dolor sit amet, consectetur adipiscing elit."
+ "Etiam velit sapien, egestas sit amet dictum eget, condimentum a ligula."
: "Texte explicatif du interoperable mode : lorem ipsum dolor sit amet, consectetur adipiscing elit."
+ " Etiam velit sapien, egestas sit amet dictum eget, condimentum a ligula."),
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_close"),
actionSecondButton: {
self.isShowPopup.toggle()
}
PopupView(
isShowPopup: $isShowPopup,
title: Text(isShowPopupForDefault ? "Default mode" : "Interoperable mode"),
content: Text(
isShowPopupForDefault
? "Texte explicatif du default mode : lorem ipsum dolor sit amet, consectetur adipiscing elit."
+ "Etiam velit sapien, egestas sit amet dictum eget, condimentum a ligula."
: "Texte explicatif du interoperable mode : lorem ipsum dolor sit amet, consectetur adipiscing elit."
+ " Etiam velit sapien, egestas sit amet dictum eget, condimentum a ligula."),
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: nil,
actionSecondButton: {},
titleThirdButton: Text("dialog_close"),
actionThirdButton: { self.isShowPopup.toggle() }
)
.background(.black.opacity(0.65))
.onTapGesture {

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
@ -55,22 +57,21 @@ struct RegisterFragment: View {
let titlePopup = Text("assistant_dialog_confirm_phone_number_title")
let contentPopup = Text(String(format: String(localized: "assistant_dialog_confirm_phone_number_message"), registerViewModel.phoneNumber))
PopupView(
isShowPopup: $isShowPopup,
title: titlePopup,
content: contentPopup,
titleFirstButton: Text("dialog_cancel"),
actionFirstButton: {
self.isShowPopup = false
},
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_continue"),
actionSecondButton: {
self.isShowPopup = false
registerViewModel.createInProgress = true
registerViewModel.startAccountCreation()
registerViewModel.phoneNumberConfirmedByUser()
}
},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: { self.isShowPopup = false },
)
.background(.black.opacity(0.65))
.onTapGesture {
@ -269,13 +270,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 +348,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)
@ -526,8 +526,10 @@ struct CallView: View {
.padding(.top)
.default_text_style_white(styleSize: 22)
Text(callViewModel.remoteAddressCleanedString)
.default_text_style_white_300(styleSize: 16)
if !CorePreferences.hideSipAddresses {
Text(callViewModel.remoteAddressCleanedString)
.default_text_style_white_300(styleSize: 16)
}
Spacer()
}
@ -558,6 +560,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 +591,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 +714,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 +913,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 +997,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 +1167,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 +1391,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 +1632,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)
@ -1917,36 +1956,38 @@ struct CallView: View {
Spacer()
ZStack {
Button {
if optionsChangeLayout == 3 {
optionsChangeLayout = 2
callViewModel.toggleVideoMode(isAudioOnlyMode: false)
} else {
callViewModel.displayMyVideo()
if !SharedMainViewModel.shared.disableVideoCall {
ZStack {
Button {
if optionsChangeLayout == 3 {
optionsChangeLayout = 2
callViewModel.toggleVideoMode(isAudioOnlyMode: false)
} else {
callViewModel.displayMyVideo()
}
} label: {
HStack {
Image(callViewModel.videoDisplayed ? "video-camera" : "video-camera-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
} label: {
HStack {
Image(callViewModel.videoDisplayed ? "video-camera" : "video-camera-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
.buttonStyle(PressedButtonStyle(buttonSize: buttonSize))
.frame(width: buttonSize, height: buttonSize)
.background(Color.gray500)
.cornerRadius(40)
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote || telecomManager.outgoingCallStarted || optionsChangeLayout == 3)
if callViewModel.isPaused || telecomManager.isPausedByRemote || telecomManager.outgoingCallStarted || optionsChangeLayout == 3 {
Color.gray600.opacity(0.8)
.cornerRadius(40)
.allowsHitTesting(false)
}
}
.buttonStyle(PressedButtonStyle(buttonSize: buttonSize))
.frame(width: buttonSize, height: buttonSize)
.background(Color.gray500)
.cornerRadius(40)
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
if callViewModel.isPaused || telecomManager.isPausedByRemote {
Color.gray600.opacity(0.8)
.cornerRadius(40)
.allowsHitTesting(false)
}
}
.frame(width: buttonSize, height: buttonSize)
Button {
callViewModel.toggleMuteMicrophone()
@ -2219,7 +2260,7 @@ struct CallView: View {
changeLayoutSheet = true
} label: {
HStack {
Image("notebook")
Image("layout")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
@ -2648,7 +2689,7 @@ struct CallView: View {
changeLayoutSheet = true
} label: {
HStack {
Image("notebook")
Image("layout")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)

View file

@ -103,17 +103,21 @@ struct CallsListFragment: View {
.background(.white)
if self.isShowPopup {
PopupView(isShowPopup: $isShowPopup,
title: Text("calls_list_dialog_merge_into_conference_title"),
content: nil,
titleFirstButton: Text("dialog_cancel"),
actionFirstButton: {self.isShowPopup.toggle()},
titleSecondButton: Text("calls_list_dialog_merge_into_conference_label"),
actionSecondButton: {
callViewModel.mergeCallsIntoConference()
self.isShowPopup.toggle()
isShowCallsListFragment.toggle()
})
PopupView(
isShowPopup: $isShowPopup,
title: Text("calls_list_dialog_merge_into_conference_title"),
content: nil,
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("calls_list_dialog_merge_into_conference_label"),
actionSecondButton: {
callViewModel.mergeCallsIntoConference()
self.isShowPopup.toggle()
isShowCallsListFragment.toggle()
},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: { self.isShowPopup.toggle() },
)
.background(.black.opacity(0.65))
.onTapGesture {
self.isShowPopup.toggle()

View file

@ -108,17 +108,21 @@ struct ParticipantsListFragment: View {
if self.isShowPopup {
let contentPopup = Text(String(format: String(localized: "meeting_call_remove_participant_confirmation_message"), callViewModel.participantList[indexToRemove].name))
PopupView(isShowPopup: $isShowPopup,
title: Text("meeting_call_remove_participant_confirmation_title"),
content: contentPopup,
titleFirstButton: Text("dialog_no"),
actionFirstButton: {self.isShowPopup.toggle()},
titleSecondButton: Text("dialog_yes"),
actionSecondButton: {
callViewModel.removeParticipant(index: indexToRemove)
self.isShowPopup.toggle()
indexToRemove = -1
})
PopupView(
isShowPopup: $isShowPopup,
title: Text("meeting_call_remove_participant_confirmation_title"),
content: contentPopup,
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_yes"),
actionSecondButton: {
callViewModel.removeParticipant(index: indexToRemove)
self.isShowPopup.toggle()
indexToRemove = -1
},
titleThirdButton: Text("dialog_no"),
actionThirdButton: { self.isShowPopup.toggle() }
)
.background(.black.opacity(0.65))
.onTapGesture {
self.isShowPopup.toggle()

View file

@ -48,7 +48,7 @@ struct MeetingWaitingRoomFragment: View {
.sheet(isPresented: $audioRouteSheet, onDismiss: {
audioRouteSheet = false
}, content: {
innerBottomSheet().presentationDetents([.fraction(0.3)])
innerBottomSheet().presentationDetents([.fraction(0.4)])
})
.onAppear {
meetingWaitingRoomViewModel.enableAVAudioSession()
@ -266,23 +266,25 @@ struct MeetingWaitingRoomFragment: View {
HStack {
Spacer()
Button {
!meetingWaitingRoomViewModel.videoDisplayed
? meetingWaitingRoomViewModel.enableVideoPreview() : meetingWaitingRoomViewModel.disableVideoPreview()
} label: {
HStack {
Image(meetingWaitingRoomViewModel.videoDisplayed ? "video-camera" : "video-camera-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
if !SharedMainViewModel.shared.disableVideoCall {
Button {
!meetingWaitingRoomViewModel.videoDisplayed
? meetingWaitingRoomViewModel.enableVideoPreview() : meetingWaitingRoomViewModel.disableVideoPreview()
} label: {
HStack {
Image(meetingWaitingRoomViewModel.videoDisplayed ? "video-camera" : "video-camera-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle(buttonSize: 60))
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
.padding(.horizontal, 5)
}
.buttonStyle(PressedButtonStyle(buttonSize: 60))
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
.padding(.horizontal, 5)
Button {
meetingWaitingRoomViewModel.toggleMuteMicrophone()

View file

@ -178,7 +178,9 @@ class CallViewModel: ObservableObject {
let friend = ContactsManager.shared.getFriendWithAddress(address: self.currentCall!.remoteAddress)
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
displayNameTmp = friend!.address!.displayName!
} else {
} else if friend != nil && friend?.name != nil {
displayNameTmp = friend?.name ?? "No name"
} else {
if self.currentCall!.remoteAddress!.displayName != nil {
displayNameTmp = self.currentCall!.remoteAddress!.displayName!
} else if self.currentCall!.remoteAddress!.username != nil && displayNameTmp.isEmpty {
@ -292,6 +294,10 @@ class CallViewModel: ObservableObject {
self.zrtpPopupDisplayed = true
}
}
}, onStateChanged: { (_: Call, state: Call.State, message: String) in
if let currentParamsTmp = self.currentCall?.currentParams, state == .StreamsRunning, currentParamsTmp.mediaEncryption == .None || currentParamsTmp.mediaEncryption == .SRTP {
self.updateEncryption(withToast: false)
}
}, onStatsUpdated: { (_: Call, stats: CallStats) in
DispatchQueue.main.async {
if self.currentCall != nil {
@ -713,44 +719,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 +992,11 @@ class CallViewModel: ObservableObject {
self.isNotEncrypted = false
}
case MediaEncryption.None:
let isNotEncryptedTmp = self.currentCall?.state == .StreamsRunning && !self.telecomManager.outgoingCallStarted
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

@ -69,8 +69,8 @@ class MeetingWaitingRoomViewModel: ObservableObject {
let confNameTmp = conf?.subject ?? "Conference"
var userNameTmp = ""
let friend = core.defaultAccount != nil && core.defaultAccount!.contactAddress != nil
? ContactsManager.shared.getFriendWithAddress(address: core.defaultAccount?.contactAddress)
let friend = core.defaultAccount != nil && core.defaultAccount!.params?.identityAddress != nil
? ContactsManager.shared.getFriendWithAddress(address: core.defaultAccount?.params?.identityAddress)
: nil
let addressTmp = friend?.address?.asStringUriOnly() ?? ""
@ -78,13 +78,13 @@ class MeetingWaitingRoomViewModel: ObservableObject {
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
userNameTmp = friend!.address!.displayName!
} else {
if core.defaultAccount != nil && core.defaultAccount!.contactAddress != nil {
if core.defaultAccount!.contactAddress!.displayName != nil {
userNameTmp = core.defaultAccount!.contactAddress!.displayName!
} else if core.defaultAccount!.contactAddress!.username != nil {
userNameTmp = core.defaultAccount!.contactAddress!.username!
if core.defaultAccount != nil && core.defaultAccount!.params?.identityAddress != nil {
if core.defaultAccount!.params?.identityAddress!.displayName != nil {
userNameTmp = core.defaultAccount!.params!.identityAddress!.displayName!
} else if core.defaultAccount!.params?.identityAddress!.username != nil {
userNameTmp = core.defaultAccount!.params!.identityAddress!.username!
} else {
userNameTmp = String(core.defaultAccount!.contactAddress!.asStringUriOnly().dropFirst(4))
userNameTmp = String(core.defaultAccount!.params!.identityAddress!.asStringUriOnly().dropFirst(4))
}
}
}
@ -92,7 +92,7 @@ class MeetingWaitingRoomViewModel: ObservableObject {
let avatarModelTmp = friend != nil
? ContactsManager.shared.avatarListModel.first(where: {
$0.friend!.name == friend!.name
&& $0.friend!.address!.asStringUriOnly() == core.defaultAccount!.contactAddress!.asStringUriOnly()
&& $0.friend!.address!.asStringUriOnly() == core.defaultAccount!.params?.identityAddress!.asStringUriOnly()
}) ?? ContactAvatarModel(friend: nil, name: userNameTmp, address: addressTmp, withPresence: false)
: ContactAvatarModel(friend: nil, name: userNameTmp, address: addressTmp, withPresence: false)

View file

@ -32,22 +32,24 @@ struct ContactsView: View {
ZStack(alignment: .bottomTrailing) {
ContactsFragment(isShowDeletePopup: $isShowDeletePopup, text: $text)
Button {
withAnimation {
contactsListViewModel.selectedEditFriend = nil
isShowEditContactFragment.toggle()
if !CorePreferences.disableAddContact {
Button {
withAnimation {
contactsListViewModel.selectedEditFriend = nil
isShowEditContactFragment.toggle()
}
} label: {
Image("user-plus")
.renderingMode(.template)
.foregroundStyle(.white)
.padding()
.background(Color.orangeMain500)
.clipShape(Circle())
.shadow(color: .black.opacity(0.2), radius: 4)
}
} label: {
Image("user-plus")
.renderingMode(.template)
.foregroundStyle(.white)
.padding()
.background(Color.orangeMain500)
.clipShape(Circle())
.shadow(color: .black.opacity(0.2), radius: 4)
.padding()
}
.padding()
// For testing crashlytics
/*Button(action: CoreContext.shared.crashForCrashlytics, label: {

View file

@ -39,127 +39,152 @@ struct ContactInnerActionsFragment: View {
var actionEditButton: () -> Void
var body: some View {
HStack(alignment: .center) {
Text("contact_details_numbers_and_addresses_title")
.default_text_style_800(styleSize: 15)
Spacer()
Image(informationIsOpen ? "caret-up" : "caret-down")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
.padding(.top, 30)
.padding(.bottom, 10)
.padding(.horizontal, 16)
.background(Color.gray100)
.onTapGesture {
withAnimation {
informationIsOpen.toggle()
}
}
if informationIsOpen {
VStack(spacing: 0) {
ForEach(0..<contactAvatarModel.addresses.count, id: \.self) { index in
HStack {
HStack {
VStack {
Text(String(localized: "sip_address") + ":")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Text(contactAvatarModel.addresses[index].dropFirst(4))
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.background(.white)
.onTapGesture {
do {
let address = try Factory.Instance.createAddress(addr: contactAvatarModel.addresses[index])
withAnimation {
telecomManager.doCallOrJoinConf(address: address)
}
} catch {
Log.error("[ContactInnerActionsFragment] unable to create address for a new outgoing call : \(contactAvatarModel.addresses[index]) \(error) ")
}
}
.onLongPressGesture(minimumDuration: 0.2) {
contactsListViewModel.stringToCopy = contactAvatarModel.addresses[index]
showingSheet.toggle()
}
if !contactAvatarModel.phoneNumbersWithLabel.isEmpty
|| index < contactAvatarModel.addresses.count - 1 {
VStack {
Divider()
}
.padding(.horizontal)
}
}
var body: some View {
if !CorePreferences.hideSipAddresses || (CorePreferences.hideSipAddresses && !contactAvatarModel.phoneNumbersWithLabel.isEmpty) {
HStack(alignment: .center) {
Text("contact_details_numbers_and_addresses_title")
.default_text_style_800(styleSize: 15)
ForEach(contactAvatarModel.phoneNumbersWithLabel.indices, id: \.self) { index in
let entry = contactAvatarModel.phoneNumbersWithLabel[index]
HStack {
HStack {
VStack {
if !entry.label.isEmpty {
Text(String(localized: "phone_number") + " (\(entry.label)):")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
Text(String(localized: "phone_number") + ":")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
Image(informationIsOpen ? "caret-up" : "caret-down")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
.padding(.top, 30)
.padding(.bottom, 10)
.padding(.horizontal, 16)
.background(Color.gray100)
.onTapGesture {
withAnimation {
informationIsOpen.toggle()
}
}
if informationIsOpen {
VStack(spacing: 0) {
if !CorePreferences.hideSipAddresses {
ForEach(0..<contactAvatarModel.addresses.count, id: \.self) { index in
HStack {
HStack {
VStack {
Text(String(localized: "sip_address") + ":")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Text(contactAvatarModel.addresses[index].dropFirst(4))
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
}
Text(entry.phoneNumber)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.background(.white)
.onTapGesture {
CoreContext.shared.doOnCoreQueue { core in
do {
let address = try Factory.Instance.createAddress(addr: contactAvatarModel.addresses[index])
telecomManager.doCallOrJoinConf(address: address)
} catch {
Log.error("[ContactInnerActionsFragment] unable to create address for a new outgoing call : \(contactAvatarModel.addresses[index]) \(error) ")
}
}
}
.onLongPressGesture(minimumDuration: 0.2) {
contactsListViewModel.stringToCopy = contactAvatarModel.addresses[index]
showingSheet.toggle()
}
if !contactAvatarModel.phoneNumbersWithLabel.isEmpty
|| index < contactAvatarModel.addresses.count - 1 {
VStack {
Divider()
}
.padding(.horizontal)
}
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.background(.white)
.onLongPressGesture(minimumDuration: 0.2) {
contactsListViewModel.stringToCopy = entry.phoneNumber
showingSheet.toggle()
}
if index < contactAvatarModel.phoneNumbersWithLabel.count - 1 {
VStack {
Divider()
ForEach(contactAvatarModel.phoneNumbersWithLabel.indices, id: \.self) { index in
let entry = contactAvatarModel.phoneNumbersWithLabel[index]
HStack {
HStack {
VStack {
if !entry.label.isEmpty {
Text(String(localized: "phone_number") + " (\(entry.label)):")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
Text(String(localized: "phone_number") + ":")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
}
Text(entry.phoneNumber)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.background(.white)
.onTapGesture {
CoreContext.shared.doOnCoreQueue { core in
let address = core.interpretUrl(url: contactAvatarModel.phoneNumbersWithLabel[index].phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core))
if address != nil {
TelecomManager.shared.doCallOrJoinConf(address: address!)
}
}
}
.onLongPressGesture(minimumDuration: 0.2) {
contactsListViewModel.stringToCopy = entry.phoneNumber
showingSheet.toggle()
}
if index < contactAvatarModel.phoneNumbersWithLabel.count - 1 {
VStack {
Divider()
}
.padding(.horizontal)
}
.padding(.horizontal)
}
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
} else {
HStack {}
.frame(height: 20)
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
if !contactAvatarModel.organization.isEmpty || !contactAvatarModel.jobTitle.isEmpty {
VStack {
@ -203,33 +228,11 @@ struct ContactInnerActionsFragment: View {
.background(Color.gray100)
VStack(spacing: 0) {
if !contactAvatarModel.nativeUri.isEmpty {
Button {
actionEditButton()
} label: {
HStack {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text("contact_details_edit")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
} else {
NavigationLink(destination: EditContactFragment(
contactAvatarModel: contactAvatarModel,
isShowEditContactFragment: $isShowEditContactFragmentInContactDetails,
isShowDismissPopup: $isShowDismissPopup)) {
if !contactAvatarModel.isReadOnly {
if !contactAvatarModel.editable {
Button {
actionEditButton()
} label: {
HStack {
Image("pencil-simple")
.renderingMode(.template)
@ -247,47 +250,71 @@ struct ContactInnerActionsFragment: View {
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
} else {
NavigationLink(destination: EditContactFragment(
contactAvatarModel: contactAvatarModel,
isShowEditContactFragment: $isShowEditContactFragmentInContactDetails,
isShowDismissPopup: $isShowDismissPopup)) {
HStack {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text("contact_details_edit")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.simultaneousGesture(
TapGesture().onEnded {
isShowEditContactFragmentInContactDetails = true
}
)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
contactsListViewModel.toggleStarredSelectedFriend()
} label: {
HStack {
Image(contactAvatarModel.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundStyle(contactAvatarModel.starred == true ? Color.redDanger500 : Color.grayMain2c500)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text(contactAvatarModel.starred == true
? "contact_details_remove_from_favourites"
: "contact_details_add_to_favourites")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
VStack {
Divider()
}
.padding(.horizontal)
Button {
contactsListViewModel.toggleStarredSelectedFriend()
} label: {
HStack {
Image(contactAvatarModel.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundStyle(contactAvatarModel.starred == true ? Color.redDanger500 : Color.grayMain2c500)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text(contactAvatarModel.starred == true
? "contact_details_remove_from_favourites"
: "contact_details_add_to_favourites")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
showShareSheet.toggle()
} label: {
@ -310,32 +337,34 @@ struct ContactInnerActionsFragment: View {
.padding(.horizontal, 20)
}
VStack {
Divider()
}
.padding(.horizontal)
Button {
isShowDeletePopup.toggle()
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text("contact_details_delete")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
if !contactAvatarModel.isReadOnly {
VStack {
Divider()
}
.padding(.horizontal)
Button {
isShowDeletePopup.toggle()
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text("contact_details_delete")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
}
.background(.white)

View file

@ -72,23 +72,11 @@ struct ContactInnerFragment: View {
Spacer()
if !contactAvatarModel.nativeUri.isEmpty {
Button(action: {
editNativeContact()
}, label: {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
.padding(.top, 2)
})
} else {
NavigationLink(destination: EditContactFragment(
contactAvatarModel: contactAvatarModel,
isShowEditContactFragment: $isShowEditContactFragmentInContactDetails,
isShowDismissPopup: $isShowDismissPopup)) {
if !contactAvatarModel.isReadOnly {
if !contactAvatarModel.editable {
Button(action: {
editNativeContact()
}, label: {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
@ -96,12 +84,26 @@ struct ContactInnerFragment: View {
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
.padding(.top, 2)
}
.simultaneousGesture(
TapGesture().onEnded {
isShowEditContactFragmentInContactDetails = true
})
} else {
NavigationLink(destination: EditContactFragment(
contactAvatarModel: contactAvatarModel,
isShowEditContactFragment: $isShowEditContactFragmentInContactDetails,
isShowDismissPopup: $isShowDismissPopup)) {
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
.padding(.top, 2)
}
)
.simultaneousGesture(
TapGesture().onEnded {
isShowEditContactFragmentInContactDetails = true
}
)
}
}
}
.frame(maxWidth: .infinity)
@ -142,16 +144,24 @@ struct ContactInnerFragment: View {
Spacer()
Button(action: {
if contactAvatarModel.addresses.count <= 1 {
do {
let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address)
telecomManager.doCallOrJoinConf(address: address, isVideo: false)
} catch {
Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ")
CoreContext.shared.doOnCoreQueue { core in
if contactAvatarModel.addresses.count == 1 {
do {
let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address)
telecomManager.doCallOrJoinConf(address: address, isVideo: false)
} catch {
Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ")
}
} else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 {
if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) {
telecomManager.doCallOrJoinConf(address: address, isVideo: false)
}
} else {
DispatchQueue.main.async {
isShowSipAddressesPopupType = 0
isShowSipAddressesPopup = true
}
}
} else {
isShowSipAddressesPopupType = 0
isShowSipAddressesPopup = true
}
}, label: {
VStack {
@ -159,88 +169,103 @@ struct ContactInnerFragment: View {
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(contactAvatarModel.address.isEmpty ? Color.grayMain2c400 : Color.grayMain2c600)
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
}
.padding(16)
.background(contactAvatarModel.address.isEmpty ? Color.grayMain2c100 : Color.grayMain2c200)
.background(Color.grayMain2c200)
.cornerRadius(40)
Text("contact_call_action")
.default_text_style(styleSize: 14)
}
})
.disabled(contactAvatarModel.address.isEmpty)
if !CorePreferences.disableChatFeature {
Spacer()
Button(action: {
if contactAvatarModel.addresses.count <= 1 {
do {
let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address)
contactsListViewModel.createOneToOneChatRoomWith(remote: address)
} catch {
Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ")
}
} else {
isShowSipAddressesPopupType = 1
isShowSipAddressesPopup = true
}
CoreContext.shared.doOnCoreQueue { core in
if contactAvatarModel.addresses.count == 1 {
do {
let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address)
contactsListViewModel.createOneToOneChatRoomWith(remote: address)
} catch {
Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ")
}
} else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 {
if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) {
contactsListViewModel.createOneToOneChatRoomWith(remote: address)
}
} else {
DispatchQueue.main.async {
isShowSipAddressesPopupType = 1
isShowSipAddressesPopup = true
}
}
}
}, label: {
VStack {
HStack(alignment: .center) {
Image("chat-teardrop-text")
.renderingMode(.template)
.resizable()
.foregroundStyle(contactAvatarModel.address.isEmpty ? Color.grayMain2c400 : Color.grayMain2c600)
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
}
.padding(16)
.background(contactAvatarModel.address.isEmpty ? Color.grayMain2c100 : Color.grayMain2c200)
.background(Color.grayMain2c200)
.cornerRadius(40)
Text("contact_message_action")
.default_text_style(styleSize: 14)
}
})
.disabled(contactAvatarModel.address.isEmpty)
}
Spacer()
Button(action: {
if contactAvatarModel.addresses.count <= 1 {
do {
let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address)
telecomManager.doCallOrJoinConf(address: address, isVideo: true)
} catch {
Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ")
if !SharedMainViewModel.shared.disableVideoCall {
Button(action: {
CoreContext.shared.doOnCoreQueue { core in
if contactAvatarModel.addresses.count == 1 {
do {
let address = try Factory.Instance.createAddress(addr: contactAvatarModel.address)
telecomManager.doCallOrJoinConf(address: address, isVideo: true)
} catch {
Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ")
}
} else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 {
if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) {
telecomManager.doCallOrJoinConf(address: address, isVideo: true)
}
} else {
DispatchQueue.main.async {
isShowSipAddressesPopupType = 2
isShowSipAddressesPopup = true
}
}
}
} else {
isShowSipAddressesPopupType = 2
isShowSipAddressesPopup = true
}
}, label: {
VStack {
HStack(alignment: .center) {
Image("video-camera")
.renderingMode(.template)
.resizable()
.foregroundStyle(contactAvatarModel.address.isEmpty ? Color.grayMain2c400 : Color.grayMain2c600)
.frame(width: 25, height: 25)
}, label: {
VStack {
HStack(alignment: .center) {
Image("video-camera")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
}
.padding(16)
.background(Color.grayMain2c200)
.cornerRadius(40)
Text("contact_video_call_action")
.default_text_style(styleSize: 14)
}
.padding(16)
.background(contactAvatarModel.address.isEmpty ? Color.grayMain2c100 : Color.grayMain2c200)
.cornerRadius(40)
Text("contact_video_call_action")
.default_text_style(styleSize: 14)
}
})
.disabled(contactAvatarModel.address.isEmpty)
Spacer()
})
Spacer()
}
}
.padding(.top, 20)
.frame(maxWidth: .infinity)

View file

@ -52,7 +52,6 @@ struct ContactListBottomSheet: View {
.padding(.trailing)
}
Spacer()
Button {
UIPasteboard.general.setValue(
contactsListViewModel.stringToCopy.prefix(4) == "sip:"

View file

@ -41,7 +41,7 @@ struct ContactsFragment: View {
showingSheet: $showingSheet,
showShareSheet: $showShareSheet
)
.presentationDetents([.fraction(0.3)])
.presentationDetents(contactsListViewModel.selectedFriend?.isReadOnly == true ? [.fraction(0.1)] : [.fraction(0.4)])
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(friendToShare: contactsListViewModel.selectedFriendToShare!)

View file

@ -22,7 +22,9 @@ import linphonesw
struct ContactsInnerFragment: View {
@ObservedObject var sharedMainViewModel = SharedMainViewModel.shared
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@EnvironmentObject var contactsListViewModel: ContactsListViewModel
@ -32,71 +34,84 @@ struct ContactsInnerFragment: View {
@Binding var text: String
var body: some View {
VStack(alignment: .leading) {
if contactsManager.avatarListModel.contains(where: { $0.starred }) {
HStack(alignment: .center) {
Text("contacts_list_favourites_title")
.default_text_style_800(styleSize: 16)
Spacer()
Image(isFavoriteOpen ? "caret-up" : "caret-down")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
.padding(.top, 10)
.padding(.horizontal, 16)
.background(.white)
.onTapGesture {
withAnimation {
isFavoriteOpen.toggle()
ZStack {
VStack(alignment: .leading) {
if contactsManager.avatarListModel.contains(where: { $0.starred }) {
HStack(alignment: .center) {
Text("contacts_list_favourites_title")
.default_text_style_800(styleSize: 16)
Spacer()
Image(isFavoriteOpen ? "caret-up" : "caret-down")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
}
if isFavoriteOpen {
FavoriteContactsListFragment(showingSheet: $showingSheet)
.zIndex(-1)
.transition(.move(edge: .top))
}
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.top, 10)
.padding(.horizontal, 16)
}
VStack {
List {
ContactsListFragment(showingSheet: $showingSheet, startCallFunc: {_ in })}
.safeAreaInset(edge: .top, content: {
Spacer()
.frame(height: 12)
})
.listStyle(.plain)
.overlay(
VStack {
if contactsManager.avatarListModel.isEmpty {
Spacer()
Image("illus-belledonne")
.resizable()
.scaledToFit()
.clipped()
.padding(.all)
Text(!text.isEmpty ? "list_filter_no_result_found" : "contacts_list_empty")
.default_text_style_800(styleSize: 16)
Spacer()
Spacer()
.padding(.top, 10)
.padding(.horizontal, 16)
.background(.white)
.onTapGesture {
withAnimation {
isFavoriteOpen.toggle()
}
}
.padding(.all)
)
if isFavoriteOpen {
FavoriteContactsListFragment(showingSheet: $showingSheet)
.zIndex(-1)
.transition(.move(edge: .top))
}
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.top, 10)
.padding(.horizontal, 16)
}
VStack {
List {
ContactsListFragment(showingSheet: $showingSheet, startCallFunc: {_ in })}
.safeAreaInset(edge: .top, content: {
Spacer()
.frame(height: 12)
})
.listStyle(.plain)
.if(sharedMainViewModel.cardDavFriendsListsCount > 0) { view in
view.refreshable {
contactsManager.refreshCardDavContacts()
}
}
.overlay(
VStack {
if contactsManager.avatarListModel.isEmpty {
Spacer()
Image("illus-belledonne")
.resizable()
.scaledToFit()
.clipped()
.padding(.all)
Text(!text.isEmpty ? "list_filter_no_result_found" : "contacts_list_empty")
.default_text_style_800(styleSize: 16)
Spacer()
Spacer()
}
}
.padding(.all)
)
}
}
if magicSearch.isLoading {
ProgressView()
.controlSize(.large)
.progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500))
}
}
.navigationBarHidden(true)

View file

@ -55,49 +55,51 @@ struct ContactsListBottomSheet: View {
.padding(.trailing)
}
Spacer()
Button {
self.contactsListViewModel.toggleStarredSelectedFriend()
if !contactsListViewModel.selectedFriend!.isReadOnly {
Spacer()
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
Button {
self.contactsListViewModel.toggleStarredSelectedFriend()
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
} label: {
HStack {
Image(contactsListViewModel.selectedFriend != nil && contactsListViewModel.selectedFriend!.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundStyle(
contactsListViewModel.selectedFriend != nil && contactsListViewModel.selectedFriend!.starred == true
? Color.redDanger500
: Color.grayMain2c500
)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text(contactsListViewModel.selectedFriend != nil && contactsListViewModel.selectedFriend!.starred == true
? "contact_details_remove_from_favourites"
: "contact_details_add_to_favourites")
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
} label: {
HStack {
Image(contactsListViewModel.selectedFriend != nil && contactsListViewModel.selectedFriend!.starred == true ? "heart-fill" : "heart")
.renderingMode(.template)
.resizable()
.foregroundStyle(
contactsListViewModel.selectedFriend != nil && contactsListViewModel.selectedFriend!.starred == true
? Color.redDanger500
: Color.grayMain2c500
)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text(contactsListViewModel.selectedFriend != nil && contactsListViewModel.selectedFriend!.starred == true
? "contact_details_remove_from_favourites"
: "contact_details_add_to_favourites")
.default_text_style(styleSize: 16)
Spacer()
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxHeight: .infinity)
.frame(maxWidth: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
if #available(iOS 16.0, *) {
@ -135,45 +137,46 @@ struct ContactsListBottomSheet: View {
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
if contactsListViewModel.selectedFriend != nil {
isShowDeletePopup.toggle()
if !contactsListViewModel.selectedFriend!.isReadOnly {
VStack {
Divider()
}
.frame(maxWidth: .infinity)
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
Button {
if contactsListViewModel.selectedFriend != nil {
isShowDeletePopup.toggle()
}
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("contact_details_delete")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("contact_details_delete")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
.padding(.horizontal, 30)
.background(Color.gray100)
}
.padding(.horizontal, 30)
.background(Color.gray100)
}
.background(Color.gray100)
.frame(maxWidth: .infinity)

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,91 @@ 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: CorePreferences.friendListInWhichStoreNewlyCreatedFriends
)
}
}
}
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,
editingFriend: true
) {
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

@ -32,6 +32,8 @@ class ContactAvatarModel: ObservableObject, Identifiable {
@Published var phoneNumbersWithLabel: [(label: String, phoneNumber: String)] = []
var nativeUri: String = ""
var editable: Bool = true
var isReadOnly: Bool = false
var withPresence: Bool?
@Published var starred: Bool = false
@ -70,12 +72,20 @@ class ContactAvatarModel: ObservableObject, Identifiable {
}
}
let nativeUriTmp = friend?.nativeUri ?? ""
let editableTmp = friend?.friendList?.type == .CardDAV || nativeUriTmp.isEmpty
let isReadOnlyTmp = (friend?.isReadOnly == true) || (friend?.inList() == false)
let withPresenceTmp = withPresence
let starredTmp = friend?.starred ?? false
let vcardTmp = friend?.vcard ?? nil
let organizationTmp = friend?.organization ?? ""
let jobTitleTmp = friend?.jobTitle ?? ""
let photoTmp = friend?.photo ?? ""
var photoTmp = friend?.photo ?? ""
if friend?.friendList?.type == .CardDAV && friend?.photo?.isEmpty == false {
let fileName = "file:/" + name + ".png"
photoTmp = fileName.replacingOccurrences(of: " ", with: "")
}
var lastPresenceInfoTmp = ""
var presenceStatusTmp: ConsolidatedPresence = .Offline
@ -108,6 +118,8 @@ class ContactAvatarModel: ObservableObject, Identifiable {
self.addresses = addressesTmp
self.phoneNumbersWithLabel = phoneNumbersWithLabelTmp
self.nativeUri = nativeUriTmp
self.editable = editableTmp
self.isReadOnly = isReadOnlyTmp
self.withPresence = withPresenceTmp
self.starred = starredTmp
self.vcard = vcardTmp

View file

@ -35,6 +35,10 @@ class ContactsListViewModel: ObservableObject {
private var contactChatRoomDelegate: ChatRoomDelegate?
private let nativeAddressBookFriendList = "Native address-book"
let linphoneAddressBookFriendList = "Linphone address-book"
let tempRemoteAddressBookFriendList = "TempRemoteDirectoryContacts address-book"
init() {}
func createOneToOneChatRoomWith(remote: Address) {

View file

@ -39,11 +39,11 @@ class EditContactViewModel: ObservableObject {
func resetValues() {
CoreContext.shared.doOnCoreQueue { _ in
let nativeUriTmp = (self.selectedEditFriend == nil ? "" : self.selectedEditFriend!.nativeUri) ?? ""
let nativeUriTmp = self.selectedEditFriend == nil ? "" : self.selectedEditFriend!.nativeUri
let givenNameTmp = (self.selectedEditFriend == nil ? "" : self.selectedEditFriend!.vcard?.givenName) ?? ""
let familyNameTmp = (self.selectedEditFriend == nil ? "" : self.selectedEditFriend!.vcard?.familyName) ?? ""
let organizationTmp = (self.selectedEditFriend == nil ? "" : self.selectedEditFriend!.organization) ?? ""
let jobTitleTmp = (self.selectedEditFriend == nil ? "" : self.selectedEditFriend!.jobTitle) ?? ""
let organizationTmp = self.selectedEditFriend == nil ? "" : self.selectedEditFriend!.organization
let jobTitleTmp = self.selectedEditFriend == nil ? "" : self.selectedEditFriend!.jobTitle
var sipAddressesTmp: [String] = []
var phoneNumbersTmp: [String] = []

View file

@ -60,11 +60,13 @@ struct ContentView: View {
@State var isShowDismissPopup = false
@State var isShowSendCancelMeetingNotificationPopup = false
@State var isShowStartCallGroupPopup = false
@State var isShowDeleteMessagePopup = false
@State var isShowSipAddressesPopup = false
@State var isShowSipAddressesPopupType = 0 // 0 to call, 1 to message, 2 to video call
@State var isShowConversationFragment = false
@State var isShowAccountProfileFragment = false
@State var isShowSettingsFragment = false
@State var isShowRecordingsListFragment = false
@State var isShowHelpFragment = false
@State var fullscreenVideo = false
@ -138,6 +140,54 @@ struct ContentView: View {
.background(Color.redDanger500)
}
if sharedMainViewModel.waitingMessageCount > 0 && (!telecomManager.callInProgress || (telecomManager.callInProgress && !telecomManager.callDisplayed)) {
HStack {
Image("voicemail")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 26, height: 26)
.padding(.leading, 10)
if sharedMainViewModel.waitingMessageCount > 1 {
Text(String(format: String(localized: "mwi_messages_are_waiting_multiple"), sharedMainViewModel.waitingMessageCount.description))
.default_text_style_white(styleSize: 16)
} else {
Text(String(localized: "mwi_messages_are_waiting_single"))
.default_text_style_white(styleSize: 16)
}
Spacer()
Button(
action: {
withAnimation {
sharedMainViewModel.waitingMessageCount = 0
}
}, label: {
Image("x")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 26, height: 26)
.padding(.trailing, 10)
}
)
}
.frame(maxWidth: .infinity)
.frame(height: 40)
.padding(.horizontal, 10)
.background(Color.gray)
.onTapGesture {
if let index = accountProfileViewModel.defaultAccountModelIndex,
index < coreContext.accounts.count {
sharedMainViewModel.waitingMessageCount = 0
coreContext.accounts[index].callVoicemailUri()
}
}
}
if !sharedMainViewModel.fileUrlsToShare.isEmpty && (!telecomManager.callInProgress || (telecomManager.callInProgress && !telecomManager.callDisplayed)) {
HStack {
Image("share-network")
@ -346,32 +396,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 +457,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 +536,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()
@ -509,7 +572,7 @@ struct ContentView: View {
Button {
sharedMainViewModel.displayedFriend = nil
isMenuOpen = false
magicSearch.allContact = true
magicSearch.changeAllContact(allContactBool: true)
magicSearch.searchForContacts()
} label: {
HStack {
@ -527,11 +590,11 @@ struct ContentView: View {
Button {
sharedMainViewModel.displayedFriend = nil
isMenuOpen = false
magicSearch.allContact = false
magicSearch.changeAllContact(allContactBool: false)
magicSearch.searchForContacts()
} label: {
HStack {
Text(String(format: String(localized: "contacts_list_filter_popup_see_linphone_only"), Bundle.main.displayName))
Text(!magicSearch.linphoneDomain ? String(localized: "contacts_list_filter_popup_see_sip_only") : String(format: String(localized: "contacts_list_filter_popup_see_linphone_only"), Bundle.main.displayName))
Spacer()
if !magicSearch.allContact {
Image("green-check")
@ -895,31 +958,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()
}
@ -972,6 +1037,7 @@ struct ContentView: View {
ConversationFragment(
isShowConversationFragment: $isShowConversationFragment,
isShowStartCallGroupPopup: $isShowStartCallGroupPopup,
isShowDeleteMessagePopup: $isShowDeleteMessagePopup,
isShowEditContactFragment: $isShowEditContactFragment,
isShowEditContactFragmentAddress: $isShowEditContactFragmentAddress,
isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment,
@ -1026,6 +1092,7 @@ struct ContentView: View {
isShowLoginFragment: $isShowLoginFragment,
isShowAccountProfileFragment: $isShowAccountProfileFragment,
isShowSettingsFragment: $isShowSettingsFragment,
isShowRecordingsListFragment: $isShowRecordingsListFragment,
isShowHelpFragment: $isShowHelpFragment
)
.environmentObject(accountProfileViewModel)
@ -1047,17 +1114,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 {
@ -1091,14 +1163,16 @@ struct ContentView: View {
)
),
content: Text("contact_dialog_delete_message"),
titleFirstButton: Text("dialog_cancel"),
actionFirstButton: {
self.isShowDeleteContactPopup.toggle()},
titleSecondButton: Text("dialog_ok"),
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_confirm"),
actionSecondButton: {
contactsListVM.deleteSelectedContact()
self.isShowDeleteContactPopup.toggle()
})
},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: { self.isShowDeleteContactPopup.toggle() }
)
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
@ -1110,27 +1184,31 @@ struct ContentView: View {
}
if isShowDeleteAllHistoryPopup {
PopupView(isShowPopup: $isShowDeleteContactPopup,
title: Text("history_dialog_delete_all_call_logs_title"),
content: Text("history_dialog_delete_all_call_logs_message"),
titleFirstButton: Text("dialog_cancel"),
actionFirstButton: {
self.isShowDeleteAllHistoryPopup.toggle()
if let historyListVM = historyListViewModel {
historyListVM.callLogsAddressToDelete = ""
PopupView(
isShowPopup: $isShowDeleteContactPopup,
title: Text("history_dialog_delete_all_call_logs_title"),
content: Text("history_dialog_delete_all_call_logs_message"),
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_confirm"),
actionSecondButton: {
if let historyListVM = historyListViewModel {
historyListVM.removeCallLogs()
}
self.isShowDeleteAllHistoryPopup.toggle()
sharedMainViewModel.displayedCall = nil
ToastViewModel.shared.toastMessage = "Success_remove_call_logs"
ToastViewModel.shared.displayToast.toggle()
},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: {
self.isShowDeleteAllHistoryPopup.toggle()
if let historyListVM = historyListViewModel {
historyListVM.callLogsAddressToDelete = ""
}
}
},
titleSecondButton: Text("dialog_ok"),
actionSecondButton: {
if let historyListVM = historyListViewModel {
historyListVM.removeCallLogs()
}
self.isShowDeleteAllHistoryPopup.toggle()
sharedMainViewModel.displayedCall = nil
ToastViewModel.shared.toastMessage = "Success_remove_call_logs"
ToastViewModel.shared.displayToast.toggle()
})
)
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
@ -1139,20 +1217,24 @@ struct ContentView: View {
}
if isShowDismissPopup {
PopupView(isShowPopup: $isShowDismissPopup,
title: Text("contact_editor_dialog_abort_confirmation_title"),
content: Text("contact_editor_dialog_abort_confirmation_message"),
titleFirstButton: Text("dialog_cancel"),
actionFirstButton: {self.isShowDismissPopup.toggle()},
titleSecondButton: Text("dialog_ok"),
actionSecondButton: {
self.isShowDismissPopup.toggle()
if isShowEditContactFragment {
isShowEditContactFragment = false
} else {
isShowEditContactFragmentInContactDetails = false
}
})
PopupView(
isShowPopup: $isShowDismissPopup,
title: Text("contact_editor_dialog_abort_confirmation_title"),
content: Text("contact_editor_dialog_abort_confirmation_message"),
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_confirm"),
actionSecondButton: {
self.isShowDismissPopup.toggle()
if isShowEditContactFragment {
isShowEditContactFragment = false
} else {
isShowEditContactFragmentInContactDetails = false
}
},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: { self.isShowDismissPopup.toggle() }
)
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
@ -1265,6 +1347,14 @@ struct ContentView: View {
.transition(.move(edge: .trailing))
}
if isShowRecordingsListFragment {
RecordingsListFragment(
isShowRecordingsListFragment: $isShowRecordingsListFragment
)
.zIndex(3)
.transition(.move(edge: .trailing))
}
if isShowHelpFragment {
HelpFragment(
isShowHelpFragment: $isShowHelpFragment
@ -1274,21 +1364,25 @@ struct ContentView: View {
}
if let meetingsListVM = meetingsListViewModel, isShowSendCancelMeetingNotificationPopup {
PopupView(isShowPopup: $isShowSendCancelMeetingNotificationPopup,
title: Text("meeting_schedule_cancel_dialog_title"),
content: !sharedMainViewModel.disableChatFeature ? Text("meeting_schedule_cancel_dialog_message") : Text(""),
titleFirstButton: Text("dialog_cancel"),
actionFirstButton: {
sharedMainViewModel.displayedMeeting = nil
meetingsListVM.deleteSelectedMeeting()
self.isShowSendCancelMeetingNotificationPopup.toggle(
) },
titleSecondButton: Text("dialog_ok"),
actionSecondButton: {
sharedMainViewModel.displayedMeeting = nil
meetingsListVM.cancelMeetingWithNotifications()
self.isShowSendCancelMeetingNotificationPopup.toggle()
})
PopupView(
isShowPopup: $isShowSendCancelMeetingNotificationPopup,
title: Text("meeting_schedule_cancel_dialog_title"),
content: !sharedMainViewModel.disableChatFeature ? Text("meeting_schedule_cancel_dialog_message") : Text(""),
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_confirm"),
actionSecondButton: {
sharedMainViewModel.displayedMeeting = nil
meetingsListVM.cancelMeetingWithNotifications()
self.isShowSendCancelMeetingNotificationPopup.toggle()
},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: {
sharedMainViewModel.displayedMeeting = nil
meetingsListVM.deleteSelectedMeeting()
self.isShowSendCancelMeetingNotificationPopup.toggle()
}
)
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
@ -1301,17 +1395,17 @@ struct ContentView: View {
isShowPopup: $isShowStartCallGroupPopup,
title: Text("conversation_info_confirm_start_group_call_dialog_title"),
content: Text("conversation_info_confirm_start_group_call_dialog_message"),
titleFirstButton: Text("dialog_cancel"),
actionFirstButton: {
self.isShowStartCallGroupPopup.toggle()
},
titleSecondButton: Text("dialog_ok"),
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_confirm"),
actionSecondButton: {
if sharedMainViewModel.displayedConversation != nil {
sharedMainViewModel.displayedConversation!.createGroupCall()
}
self.isShowStartCallGroupPopup.toggle()
}
},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: { self.isShowStartCallGroupPopup.toggle() }
)
.background(.black.opacity(0.65))
.zIndex(3)
@ -1320,6 +1414,31 @@ struct ContentView: View {
}
}
if isShowDeleteMessagePopup {
PopupView(
isShowPopup: $isShowDeleteMessagePopup,
title: Text("conversation_dialog_delete_chat_message_title"),
content: nil,
titleFirstButton: Text("conversation_dialog_delete_for_everyone_label"),
actionFirstButton: {
NotificationCenter.default.post(name: NSNotification.Name("DeleteMessageForEveryone"), object: nil)
self.isShowDeleteMessagePopup.toggle()
},
titleSecondButton: Text("conversation_dialog_delete_locally_label"),
actionSecondButton: {
NotificationCenter.default.post(name: NSNotification.Name("DeleteMessageForMe"), object: nil)
self.isShowDeleteMessagePopup.toggle()
},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: { self.isShowDeleteMessagePopup.toggle() }
)
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
self.isShowDeleteMessagePopup.toggle()
}
}
if isShowConversationInfoPopup {
PopupViewWithTextField(
isShowConversationInfoPopup: $isShowConversationInfoPopup,
@ -1388,6 +1507,8 @@ struct ContentView: View {
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("ContactLoaded"))) { _ in
callViewModel.resetCallView()
if let conversationsListVM = conversationsListViewModel {
conversationsListVM.updateChatRoomsList()
}
@ -1465,6 +1586,7 @@ struct ContentView: View {
}
}
}
.id(coreContext.reloadID)
}
func openMenu() {

View file

@ -52,7 +52,7 @@ struct ChatBubbleView: View {
HStack {
if eventLogMessage.eventModel.eventLogType == .ConferenceChatMessage {
VStack {
if !eventLogMessage.message.text.isEmpty || !eventLogMessage.message.attachments.isEmpty || eventLogMessage.message.isIcalendar {
if !eventLogMessage.message.text.isEmpty || !eventLogMessage.message.attachments.isEmpty || eventLogMessage.message.isIcalendar || eventLogMessage.message.isRetracted {
HStack(alignment: .top, content: {
if eventLogMessage.message.isOutgoing {
Spacer()
@ -137,6 +137,12 @@ struct ChatBubbleView: View {
.foregroundStyle(Color.grayMain2c700)
.default_text_style(styleSize: 14)
.lineLimit(/*@START_MENU_TOKEN@*/2/*@END_MENU_TOKEN@*/)
} else if eventLogMessage.message.replyMessage!.isRetracted {
Text(eventLogMessage.message.replyMessage!.isOutgoing ? "conversation_message_content_deleted_by_us_label" : "conversation_message_content_deleted_label")
.italic()
.foregroundStyle(Color.grayMain2c500)
.font(.system(size: 14))
.lineLimit(1)
}
}
.padding(.all, 15)
@ -173,7 +179,13 @@ struct ChatBubbleView: View {
}
if !eventLogMessage.message.text.isEmpty {
DynamicLinkText(text: eventLogMessage.message.text)
DynamicLinkText(text: eventLogMessage.message.text, participantConversationModel: conversationViewModel.participantConversationModel)
} else if eventLogMessage.message.isRetracted {
Text(eventLogMessage.message.isOutgoing ? "conversation_message_content_deleted_by_us_label" : "conversation_message_content_deleted_label")
.italic()
.foregroundStyle(Color.grayMain2c500)
.font(.system(size: 14))
.lineLimit(1)
}
if eventLogMessage.message.isIcalendar && eventLogMessage.message.messageConferenceInfo != nil {
@ -325,6 +337,14 @@ struct ChatBubbleView: View {
.padding(.top, 1)
}
if eventLogMessage.message.isEdited && eventLogMessage.message.isOutgoing {
Text("conversation_message_edited_label")
.foregroundStyle(Color.grayMain2c500)
.default_text_style_300(styleSize: 12)
.padding(.top, 1)
.padding(.trailing, -4)
}
Text(conversationViewModel.getMessageTime(startDate: eventLogMessage.message.dateReceived))
.foregroundStyle(Color.grayMain2c500)
.default_text_style_300(styleSize: 12)
@ -349,6 +369,14 @@ struct ChatBubbleView: View {
}
}
if eventLogMessage.message.isEdited && !eventLogMessage.message.isOutgoing {
Text("conversation_message_edited_label")
.foregroundStyle(Color.grayMain2c500)
.default_text_style_300(styleSize: 12)
.padding(.top, 1)
.padding(.trailing, -4)
}
if eventLogMessage.message.isEphemeral && !eventLogMessage.message.isOutgoing {
Image("clock-countdown")
.renderingMode(.template)
@ -918,42 +946,81 @@ struct ChatBubbleView: View {
struct DynamicLinkText: View {
let text: String
let participantConversationModel: [ContactAvatarModel]
var body: some View {
let components = text.components(separatedBy: " ")
Text(makeAttributedString(from: components))
Text(makeAttributedString(from: text))
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.leading)
.lineLimit(nil)
.foregroundStyle(Color.grayMain2c700)
.default_text_style(styleSize: 14)
}
// Function to create an AttributedString with clickable links
private func makeAttributedString(from components: [String]) -> AttributedString {
var result = AttributedString("")
for (index, component) in components.enumerated() {
if let url = URL(string: component.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""),
url.scheme == "http" || url.scheme == "https" {
var attributedText = AttributedString(component)
attributedText.link = url
attributedText.foregroundColor = .blue
attributedText.underlineStyle = .single
result.append(attributedText)
private func makeAttributedString(from text: String) -> AttributedString {
var result = AttributedString()
var currentWord = ""
for char in text {
if char == " " || char == "\n" {
appendWord(currentWord, to: &result)
result.append(AttributedString(String(char)))
currentWord = ""
} else {
result.append(AttributedString(component))
}
// Add space between words except for the last one
if index < components.count - 1 {
result.append(AttributedString(" "))
currentWord.append(char)
}
}
appendWord(currentWord, to: &result)
return result
}
private func appendWord(_ word: String, to result: inout AttributedString) {
guard !word.isEmpty else { return }
// URL
if
let encoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: encoded),
["http", "https"].contains(url.scheme)
{
var link = AttributedString(word)
link.link = url
link.foregroundColor = .blue
link.underlineStyle = .single
result.append(link)
return
}
// Mention
if isMention(word),
let participant = participantConversationModel.first(where: {($0.address.dropFirst(4).split(separator: "@").first ?? "") == word.dropFirst()}),
let mentionURL = URL(string: "linphone-mention://\(participant.address)")
{
var mention = AttributedString("@" + participant.name)
mention.link = mentionURL
mention.foregroundColor = Color.orangeMain500
mention.font = .system(size: 14, weight: .semibold)
result.append(mention)
return
}
// Text
var normal = AttributedString(word)
normal.foregroundColor = Color.grayMain2c700
result.append(normal)
}
private func isMention(_ word: String) -> Bool {
guard word.first == "@", word.count > 1 else { return false }
let username = word.dropFirst()
return username.allSatisfy {
$0.isLetter || $0.isNumber || $0 == "." || $0 == "_"
}
}
}
enum URLType {
case name(String) // local file name of gif
case url(URL) // remote url

View file

@ -141,50 +141,58 @@ struct ConversationForwardMessageFragment: View {
.padding(.vertical)
.padding(.horizontal)
ScrollView {
if !conversationForwardMessageViewModel.conversationsList.isEmpty {
HStack(alignment: .center) {
Text("bottom_navigation_conversations_label")
.default_text_style_800(styleSize: 16)
ZStack {
ScrollView {
if !conversationForwardMessageViewModel.conversationsList.isEmpty {
HStack(alignment: .center) {
Text("bottom_navigation_conversations_label")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
Spacer()
conversationsList
}
.padding(.vertical, 10)
if !ContactsManager.shared.lastSearch.isEmpty {
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
}
ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in
withAnimation {
conversationForwardMessageViewModel.createOneToOneChatRoomWith(remote: addr)
}
})
.padding(.horizontal, 16)
conversationsList
if !contactsManager.lastSearchSuggestions.isEmpty {
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
suggestionsList
}
}
if !ContactsManager.shared.lastSearch.isEmpty {
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
}
ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in
withAnimation {
conversationForwardMessageViewModel.createOneToOneChatRoomWith(remote: addr)
}
})
.padding(.horizontal, 16)
if !contactsManager.lastSearchSuggestions.isEmpty {
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
suggestionsList
if magicSearch.isLoading {
ProgressView()
.controlSize(.large)
.progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500))
}
}
}
@ -275,6 +283,7 @@ struct ConversationForwardMessageFragment: View {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil {
if contactsManager.lastSearchSuggestions[index].address!.domain != CorePreferences.defaultDomain {
Image(uiImage: contactsManager.textToImage(
firstName: String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)),
lastName: ""))
@ -284,9 +293,29 @@ struct ConversationForwardMessageFragment: View {
Text(String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)))
.default_text_style(styleSize: 16)
.lineLimit(1)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
} else {
if let address = contactsManager.lastSearchSuggestions[index].address {
let nameTmp = address.displayName
?? address.username
?? String(address.asStringUriOnly().dropFirst(4))
Image(uiImage: contactsManager.textToImage(
firstName: nameTmp,
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(nameTmp)
.default_text_style(styleSize: 16)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
} else {
Image("profil-picture-default")
.resizable()

View file

@ -64,6 +64,7 @@ struct ConversationFragment: View {
@Binding var isShowConversationFragment: Bool
@Binding var isShowStartCallGroupPopup: Bool
@Binding var isShowDeleteMessagePopup: Bool
@State private var selectedCategoryIndex = 0
@ -88,6 +89,20 @@ struct ConversationFragment: View {
@State private var isImdnOrReactionsSheetVisible = false
@State var mentionIsOpen: Bool = false
@State var mentionQuery: String = ""
private let rowHeight: CGFloat = 60
private let maxVisibleRows: CGFloat = 3.5
private var filteredParticipants: [ContactAvatarModel] {
conversationViewModel.participantConversationModel.filter {
mentionQuery.isEmpty
|| $0.name.localizedCaseInsensitiveContains(mentionQuery)
|| String($0.address.dropFirst(4).split(separator: "@").first ?? "").localizedCaseInsensitiveContains(mentionQuery)
}
}
var body: some View {
NavigationView {
GeometryReader { geometry in
@ -233,6 +248,8 @@ struct ConversationFragment: View {
if SharedMainViewModel.shared.displayedConversation != nil && (navigationManager.peerAddr == nil || navigationManager.peerAddr!.contains(SharedMainViewModel.shared.displayedConversation!.remoteSipUri)) {
conversationViewModel.resetDisplayedChatRoom()
}
} else {
conversationViewModel.compose(stop: true, cachedConversation: cachedConversation)
}
}
}
@ -460,6 +477,7 @@ struct ConversationFragment: View {
}
}
.onDisappear {
conversationViewModel.compose(stop: true, cachedConversation: cachedConversation)
conversationViewModel.resetMessage()
}
} else {
@ -555,6 +573,7 @@ struct ConversationFragment: View {
conversationViewModel.getMessages()
}
.onDisappear {
conversationViewModel.compose(stop: true, cachedConversation: cachedConversation)
conversationViewModel.resetMessage()
}
}
@ -620,6 +639,43 @@ struct ConversationFragment: View {
}
}
.transition(.move(edge: .bottom))
} else if conversationViewModel.messageToEdit != nil {
ZStack(alignment: .top) {
HStack {
VStack {
Text("conversation_editing_message_title")
.default_text_style_300(styleSize: 15)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 1)
.lineLimit(1)
Text("\(conversationViewModel.messageToEdit!.message.text)")
.default_text_style_300(styleSize: 15)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
}
}
.frame(maxWidth: .infinity)
.padding(.all, 20)
.background(Color.gray100)
HStack {
Spacer()
Button(action: {
messageText = ""
withAnimation {
conversationViewModel.messageToEdit = nil
}
}, label: {
Image("x")
.resizable()
.frame(width: 30, height: 30, alignment: .leading)
.padding(.all, 10)
})
}
}
.transition(.move(edge: .bottom))
}
if !conversationViewModel.mediasToSend.isEmpty || mediasIsLoading {
@ -821,6 +877,72 @@ struct ConversationFragment: View {
.transition(.move(edge: .bottom))
}
if mentionIsOpen && SharedMainViewModel.shared.displayedConversation!.isGroup {
ZStack(alignment: .top) {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
Text("conversation_participants_list_header")
.default_text_style_300(styleSize: 12)
.lineLimit(1)
.frame(height: 14)
.padding(.vertical, 8)
.padding(.horizontal, 10)
if filteredParticipants.isEmpty {
VStack {
Text("conversation_participants_list_empty")
.default_text_style_800(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .center)
}
.frame(height: rowHeight)
}
ForEach(filteredParticipants, id: \.id) { participant in
Button {
messageText = String(messageText.dropLast(mentionQuery.count))
messageText.append((participant.address.dropFirst(4).split(separator: "@").first ?? "") + " ")
} label: {
HStack {
Avatar(contactAvatarModel: participant, avatarSize: 40)
Text(participant.name)
.default_text_style(styleSize: 16)
.lineLimit(1)
Spacer()
}
.frame(maxWidth: .infinity)
.background(Color.gray100)
.padding(.horizontal)
}
.frame(height: rowHeight)
.buttonStyle(.plain)
}
}
}
.frame(
height: filteredParticipants.isEmpty ? rowHeight + 30 : min(
(CGFloat(filteredParticipants.count) * rowHeight) + 30,
(rowHeight * maxVisibleRows) + 30
)
)
.clipped()
.background(Color.gray100)
HStack {
Spacer()
Button {
withAnimation { mentionIsOpen = false }
} label: {
Image("x")
.resizable()
.frame(width: 24, height: 24)
.padding(10)
}
}
}
.transition(.move(edge: .bottom))
}
HStack(spacing: 0) {
if !voiceRecordingInProgress {
Button {
@ -847,9 +969,8 @@ struct ConversationFragment: View {
.focused($isMessageTextFocused)
.padding(.vertical, 5)
.onChange(of: messageText) { text in
if !text.isEmpty {
conversationViewModel.compose()
}
self.updateMentionState(from: text)
conversationViewModel.compose(stop: text.isEmpty)
}
} else {
ZStack(alignment: .leading) {
@ -860,9 +981,7 @@ struct ConversationFragment: View {
.default_text_style(styleSize: 15)
.focused($isMessageTextFocused)
.onChange(of: messageText) { text in
if !text.isEmpty {
conversationViewModel.compose()
}
conversationViewModel.compose(stop: text.isEmpty)
}
if messageText.isEmpty {
@ -879,43 +998,66 @@ struct ConversationFragment: View {
}
}
if messageText.isEmpty && conversationViewModel.mediasToSend.isEmpty {
Button {
voiceRecordingInProgress = true
} label: {
Image("microphone")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 28, height: 28, alignment: .leading)
.padding(.all, 6)
.padding(.top, 4)
if conversationViewModel.messageToEdit == nil {
if messageText.isEmpty && conversationViewModel.mediasToSend.isEmpty {
Button {
voiceRecordingInProgress = true
} label: {
Image("microphone")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 28, height: 28, alignment: .leading)
.padding(.all, 6)
.padding(.top, 4)
}
} else {
Button {
if conversationViewModel.displayedConversationHistorySize > 1 {
NotificationCenter.default.post(name: .onScrollToBottom, object: nil)
}
let messageTextTmp = self.messageText
messageText = " "
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
messageText = ""
isMessageTextFocused = true
conversationViewModel.sendMessage(messageText: messageTextTmp)
}
} label: {
Image("paper-plane-tilt")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 28, height: 28, alignment: .leading)
.padding(.all, 6)
.padding(.top, 4)
.rotationEffect(.degrees(45))
}
.padding(.trailing, 4)
}
} else {
Button {
if conversationViewModel.displayedConversationHistorySize > 1 {
NotificationCenter.default.post(name: .onScrollToBottom, object: nil)
}
let messageTextTmp = self.messageText
messageText = " "
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
messageText = ""
isMessageTextFocused = true
conversationViewModel.sendMessage(messageText: messageTextTmp)
}
messageText = " "
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
messageText = ""
isMessageTextFocused = true
conversationViewModel.sendMessage(messageText: messageTextTmp)
}
} label: {
Image("paper-plane-tilt")
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.foregroundStyle(messageText.isEmpty ? Color.gray300 : Color.orangeMain500)
.frame(width: 28, height: 28, alignment: .leading)
.padding(.all, 6)
.padding(.top, 4)
.rotationEffect(.degrees(45))
}
.padding(.trailing, 4)
.disabled(messageText.isEmpty)
}
}
.padding(.leading, 15)
@ -1096,28 +1238,67 @@ struct ConversationFragment: View {
Divider()
}
Button {
let indexMessage = conversationViewModel.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.id == conversationViewModel.selectedMessage!.message.id})
conversationViewModel.selectedMessage = nil
conversationViewModel.replyToMessage(index: indexMessage ?? 0, isMessageTextFocused: Binding(
get: { isMessageTextFocused },
set: { isMessageTextFocused = $0 }
))
} label: {
HStack {
Text("menu_reply_to_chat_message")
.default_text_style(styleSize: 15)
Spacer()
Image("reply")
.resizable()
.frame(width: 20, height: 20, alignment: .leading)
}
.padding(.vertical, 5)
.padding(.horizontal, 20)
}
Divider()
if conversationViewModel.selectedMessage!.message.isOutgoing
&& !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly)
&& conversationViewModel.selectedMessage!.message.isEditable {
Button {
if let chatMessage = conversationViewModel.selectedMessage {
if voiceRecordingInProgress {
voiceRecordingInProgress = false
}
messageText = chatMessage.message.text
conversationViewModel.selectedMessage = nil
conversationViewModel.editMessage(
chatMessage: chatMessage,
isMessageTextFocused: Binding(
get: { isMessageTextFocused },
set: { isMessageTextFocused = $0 }
)
)
}
} label: {
HStack {
Text("menu_edit_chat_message")
.default_text_style(styleSize: 15)
Spacer()
Image("pencil-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 20, height: 20, alignment: .leading)
}
.padding(.vertical, 5)
.padding(.horizontal, 20)
}
Divider()
}
if !conversationViewModel.selectedMessage!.message.isRetracted {
Button {
let indexMessage = conversationViewModel.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.id == conversationViewModel.selectedMessage!.message.id})
conversationViewModel.selectedMessage = nil
conversationViewModel.replyToMessage(index: indexMessage ?? 0, isMessageTextFocused: Binding(
get: { isMessageTextFocused },
set: { isMessageTextFocused = $0 }
))
} label: {
HStack {
Text("menu_reply_to_chat_message")
.default_text_style(styleSize: 15)
Spacer()
Image("reply")
.resizable()
.frame(width: 20, height: 20, alignment: .leading)
}
.padding(.vertical, 5)
.padding(.horizontal, 20)
}
Divider()
}
if !conversationViewModel.selectedMessage!.message.text.isEmpty {
Button {
@ -1146,27 +1327,35 @@ struct ConversationFragment: View {
Divider()
}
Button {
withAnimation {
isShowConversationForwardMessageFragment = true
if !conversationViewModel.selectedMessage!.message.isRetracted {
Button {
withAnimation {
isShowConversationForwardMessageFragment = true
}
} label: {
HStack {
Text("menu_forward_chat_message")
.default_text_style(styleSize: 15)
Spacer()
Image("forward")
.resizable()
.frame(width: 20, height: 20, alignment: .leading)
}
.padding(.vertical, 5)
.padding(.horizontal, 20)
}
} label: {
HStack {
Text("menu_forward_chat_message")
.default_text_style(styleSize: 15)
Spacer()
Image("forward")
.resizable()
.frame(width: 20, height: 20, alignment: .leading)
}
.padding(.vertical, 5)
.padding(.horizontal, 20)
Divider()
}
Divider()
Button {
conversationViewModel.deleteMessage()
if conversationViewModel.selectedMessage!.message.isOutgoing
&& !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly)
&& conversationViewModel.selectedMessage!.message.isRetractable && !conversationViewModel.selectedMessage!.message.isRetracted {
isShowDeleteMessagePopup = true
} else {
conversationViewModel.deleteMessage()
}
} label: {
HStack {
Text("menu_delete_selected_item")
@ -1211,11 +1400,18 @@ struct ConversationFragment: View {
}
.onAppear {
touchFeedback()
if isMessageTextFocused {
isMessageTextFocused = false
}
}
.onDisappear {
if conversationViewModel.selectedMessage != nil {
conversationViewModel.selectedMessage = nil
}
}.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("DeleteMessageForMe"))) { _ in
conversationViewModel.deleteMessage()
}.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("DeleteMessageForEveryone"))) { _ in
conversationViewModel.deleteMessageForEveryone()
}
}
@ -1263,6 +1459,41 @@ struct ConversationFragment: View {
}
}
}
func updateMentionState(from text: String) {
guard let atIndex = text.lastIndex(of: "@") else {
closeMention()
return
}
if atIndex > text.startIndex {
let before = text[text.index(before: atIndex)]
if before != " " && before != "\n" {
closeMention()
return
}
}
let query = String(text[text.index(after: atIndex)...])
if query.contains(" ") || query.contains("\n") {
closeMention()
return
}
withAnimation {
mentionQuery = query
mentionIsOpen = true
}
}
func closeMention() {
withAnimation {
mentionIsOpen = false
mentionQuery = ""
}
}
// swiftlint:enable cyclomatic_complexity
// swiftlint:enable function_body_length
}

View file

@ -104,12 +104,14 @@ struct ConversationInfoFragment: View {
.frame(maxWidth: .infinity)
.padding(.top, 10)
Text(conversationViewModel.participantConversationModel.first?.address ?? "")
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity)
.padding(.top, 5)
if !CorePreferences.hideSipAddresses {
Text(conversationViewModel.participantConversationModel.first?.address ?? "")
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity)
.padding(.top, 5)
}
if !SharedMainViewModel.shared.displayedConversation!.avatarModel.lastPresenceInfo.isEmpty {
Text(SharedMainViewModel.shared.displayedConversation!.avatarModel.lastPresenceInfo)
@ -351,59 +353,65 @@ struct ConversationInfoFragment: View {
if conversationViewModel.myParticipantConversationModel != nil && conversationViewModel.myParticipantConversationModel!.address != participantConversationModel.address {
Menu {
Button(
action: {
let addressConv = participantConversationModel.address
let friendIndex = contactsManager.avatarListModel.first(
where: {$0.addresses.contains(where: {$0 == addressConv})})
SharedMainViewModel.shared.displayedCall = nil
SharedMainViewModel.shared.changeIndexView(indexViewInt: 0)
if friendIndex != nil {
withAnimation {
SharedMainViewModel.shared.displayedFriend = friendIndex
}
} else {
withAnimation {
isShowEditContactFragment.toggle()
isShowEditContactFragmentAddress = String(participantConversationModel.address.dropFirst(4))
}
}
},
label: {
HStack {
let addressConv = participantConversationModel.address
let friendIndex = contactsManager.lastSearch.firstIndex(
where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressConv})})
let disableAddContact = CorePreferences.disableAddContact
if (!disableAddContact || (disableAddContact && friendIndex != nil)) {
Button(
action: {
let addressConv = participantConversationModel.address
let friendIndex = contactsManager.lastSearch.firstIndex(
where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressConv})})
let friendIndex = contactsManager.avatarListModel.first(
where: {$0.addresses.contains(where: {$0 == addressConv})})
SharedMainViewModel.shared.changeIndexView(indexViewInt: 0)
if friendIndex != nil {
Image("address-book")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("conversation_info_menu_go_to_contact")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
withAnimation {
SharedMainViewModel.shared.displayedFriend = friendIndex
}
} else {
Image("user-plus")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("conversation_info_menu_add_to_contacts")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
withAnimation {
isShowEditContactFragment.toggle()
isShowEditContactFragmentAddress = String(participantConversationModel.address.dropFirst(4))
}
}
SharedMainViewModel.shared.displayedConversation = nil
},
label: {
HStack {
if friendIndex != nil {
Image("address-book")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("conversation_info_menu_go_to_contact")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else {
Image("user-plus")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("conversation_info_menu_add_to_contacts")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
}
}
}
}
)
)
}
if conversationViewModel.isUserAdmin {
let participantConversationModelIsAdmin = conversationViewModel.participantConversationModelAdmin.first(
@ -525,7 +533,14 @@ struct ConversationInfoFragment: View {
VStack(spacing: 0) {
if !SharedMainViewModel.shared.displayedConversation!.isReadOnly {
if !SharedMainViewModel.shared.displayedConversation!.isGroup {
let addressConv = conversationViewModel.participantConversationModel.first?.address ?? ""
let friendIndex = contactsManager.lastSearch.firstIndex(
where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressConv})})
let disableAddContact = CorePreferences.disableAddContact
if !SharedMainViewModel.shared.displayedConversation!.isGroup && (!disableAddContact || (disableAddContact && friendIndex != nil)) {
Button(
action: {
if SharedMainViewModel.shared.displayedConversation != nil {
@ -553,10 +568,6 @@ struct ConversationInfoFragment: View {
},
label: {
HStack {
let addressConv = conversationViewModel.participantConversationModel.first?.address ?? ""
let friendIndex = contactsManager.lastSearch.firstIndex(
where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressConv})})
if friendIndex != nil {
Image("address-book")
.renderingMode(.template)

View file

@ -105,14 +105,46 @@ struct ConversationRow: View {
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
Text(conversation.lastMessageText)
.foregroundStyle(Color.grayMain2c400)
.if(conversation.unreadMessagesCount > 0) { view in
view.default_text_style_700(styleSize: 14)
HStack(spacing: 0) {
Text(conversation.lastMessagePrefixText)
.foregroundStyle(Color.grayMain2c400)
.if(conversation.unreadMessagesCount > 0) { view in
view.default_text_style_700(styleSize: 14)
}
.default_text_style(styleSize: 14)
.lineLimit(1)
.layoutPriority(1)
if !conversation.lastMessageIcon.isEmpty {
Image(conversation.lastMessageIcon)
.resizable()
.frame(width: 16, height: 16)
.layoutPriority(0)
.padding(.trailing, 2)
}
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
if conversation.lastMessageInItalic {
Text(conversation.lastMessageText)
.italic()
.if(conversation.unreadMessagesCount > 0) { view in
view.bold()
}
.foregroundStyle(Color.grayMain2c400)
.font(.system(size: 14))
.lineLimit(1)
.layoutPriority(-1)
} else {
Text(conversation.lastMessageText)
.foregroundStyle(Color.grayMain2c400)
.if(conversation.unreadMessagesCount > 0) { view in
view.default_text_style_700(styleSize: 14)
}
.default_text_style(styleSize: 14)
.lineLimit(1)
.layoutPriority(-1)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
}

View file

@ -170,34 +170,42 @@ struct StartConversationFragment: View {
)
}
ScrollView {
if !ContactsManager.shared.lastSearch.isEmpty {
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
ZStack {
ScrollView {
if !ContactsManager.shared.lastSearch.isEmpty {
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
}
ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in
ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in
startConversationViewModel.createOneToOneChatRoomWith(remote: addr)
})
.padding(.horizontal, 16)
if !contactsManager.lastSearchSuggestions.isEmpty {
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
})
.padding(.horizontal, 16)
suggestionsList
if !contactsManager.lastSearchSuggestions.isEmpty {
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
suggestionsList
}
}
if magicSearch.isLoading {
ProgressView()
.controlSize(.large)
.progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500))
}
}
}
@ -257,6 +265,7 @@ struct StartConversationFragment: View {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil {
if contactsManager.lastSearchSuggestions[index].address!.domain != CorePreferences.defaultDomain {
Image(uiImage: contactsManager.textToImage(
firstName: String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)),
lastName: ""))
@ -266,9 +275,29 @@ struct StartConversationFragment: View {
Text(String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)))
.default_text_style(styleSize: 16)
.lineLimit(1)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
} else {
if let address = contactsManager.lastSearchSuggestions[index].address {
let nameTmp = address.displayName
?? address.username
?? String(address.asStringUriOnly().dropFirst(4))
Image(uiImage: contactsManager.textToImage(
firstName: nameTmp,
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(nameTmp)
.default_text_style(styleSize: 16)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
} else {
Image("profil-picture-default")
.resizable()

View file

@ -111,7 +111,7 @@ struct StartGroupConversationFragment: View {
Button(action: {
startConversationViewModel.createGroupChatRoom()
}, label: {
Text("dialog_ok")
Text("dialog_confirm")
.default_text_style_white_600(styleSize: 20)
.frame(height: 35)
.frame(maxWidth: .infinity)

View file

@ -551,6 +551,12 @@ struct UIList: UIViewRepresentable {
}
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let eventLogMessage = parent.conversationViewModel.conversationMessagesSection[0].rows[indexPath.row]
guard !eventLogMessage.message.isRetracted && eventLogMessage.eventModel.eventLogType == .ConferenceChatMessage else {
return nil
}
let archiveAction = UIContextualAction(style: .normal, title: "") { _, _, completionHandler in
self.parent.conversationViewModel.replyToMessage(index: indexPath.row, isMessageTextFocused: Binding(
get: { self.parent.isMessageTextFocused },

View file

@ -46,9 +46,12 @@ class ConversationModel: ObservableObject, Identifiable {
@Published var isMuted: Bool
@Published var isEphemeral: Bool
@Published var encryptionEnabled: Bool
@Published var lastMessagePrefixText: String
@Published var lastMessageText: String
@Published var lastMessageIcon: String
@Published var lastMessageIsOutgoing: Bool
@Published var lastMessageState: Int
@Published var lastMessageInItalic: Bool
@Published var unreadMessagesCount: Int
@Published var avatarModel: ContactAvatarModel
@ -138,11 +141,17 @@ class ConversationModel: ObservableObject, Identifiable {
self.lastMessage = nil
self.lastMessagePrefixText = ""
self.lastMessageText = ""
self.lastMessageIcon = ""
self.lastMessageIsOutgoing = false
self.lastMessageState = 0
self.lastMessageInItalic = false
self.unreadMessagesCount = chatRoom.unreadMessagesCount
@ -294,8 +303,10 @@ class ConversationModel: ObservableObject, Identifiable {
fromAddressFriend = nil
}
var lastMessageTextTmp = (fromAddressFriend ?? "")
+ (lastMessage!.contents.first(where: {$0.isText == true})?.utf8Text ?? (lastMessage!.contents.first(where: {$0.isFile == true || $0.isFileTransfer == true})?.name ?? ""))
let lastMessagePrefixTextTmp = (fromAddressFriend ?? "")
var lastMessageTextTmp = (lastMessage!.contents.first(where: {$0.isText == true})?.utf8Text ?? (lastMessage!.contents.first(where: {$0.isFile == true || $0.isFileTransfer == true})?.name ?? ""))
var lastMessageIconTmp = ""
var lastMessageInItalicTmp = false
if lastMessage!.contents.first != nil && lastMessage!.contents.first!.isIcalendar == true {
if let conferenceInfo = try? Factory.Instance.createConferenceInfoFromIcalendarContent(content: lastMessage!.contents.first!) {
@ -308,10 +319,30 @@ class ConversationModel: ObservableObject, Identifiable {
} else if conferenceInfo.state == .Cancelled {
lastMessageTextTmp = String(localized: "message_meeting_invitation_cancelled_notification")
}
lastMessageIconTmp = "calendar"
lastMessageInItalicTmp = true
}
}
}
if (lastMessage!.contents.first(where: {$0.isFile == true || $0.isFileTransfer == true})?.name != nil) {
lastMessageIconTmp = "file"
} else if lastMessage!.isReply {
lastMessageIconTmp = "reply"
} else if lastMessage!.isForward {
lastMessageIconTmp = "forward"
}
if lastMessage!.isRetracted {
lastMessageTextTmp += lastMessage!.isOutgoing ? String(localized: "conversation_message_content_deleted_by_us_label") : String(localized: "conversation_message_content_deleted_label")
lastMessageIconTmp = "trash"
lastMessageInItalicTmp = true
}
let lastMessageIsOutgoingTmp = lastMessage?.isOutgoing ?? false
let lastUpdateTimeTmp = lastMessage?.time ?? chatRoom.lastUpdateTime
@ -319,13 +350,19 @@ class ConversationModel: ObservableObject, Identifiable {
let lastMessageStateTmp = lastMessage?.state.rawValue ?? 0
DispatchQueue.main.async {
self.lastMessagePrefixText = lastMessagePrefixTextTmp
self.lastMessageText = lastMessageTextTmp
self.lastMessageIcon = lastMessageIconTmp
self.lastMessageIsOutgoing = lastMessageIsOutgoingTmp
self.lastUpdateTime = lastUpdateTimeTmp
self.lastMessageState = lastMessageStateTmp
self.lastMessageInItalic = lastMessageInItalicTmp
}
getUnreadMessagesCount()

View file

@ -68,6 +68,10 @@ public struct Message: Identifiable, Hashable {
public var status: Status?
public var createdAt: Date
public var isOutgoing: Bool
public var isEditable: Bool
public var isRetractable: Bool
public var isEdited: Bool
public var isRetracted: Bool
public var dateReceived: time_t
public var address: String
@ -94,6 +98,10 @@ public struct Message: Identifiable, Hashable {
status: Status? = nil,
createdAt: Date = Date(),
isOutgoing: Bool,
isEditable: Bool,
isRetractable: Bool,
isEdited: Bool,
isRetracted: Bool,
dateReceived: time_t,
address: String,
isFirstMessage: Bool = false,
@ -116,6 +124,10 @@ public struct Message: Identifiable, Hashable {
self.status = status
self.createdAt = createdAt
self.isOutgoing = isOutgoing
self.isEditable = isEditable
self.isRetractable = isRetractable
self.isEdited = isEdited
self.isRetracted = isRetracted
self.dateReceived = dateReceived
self.isFirstMessage = isFirstMessage
self.address = address
@ -163,6 +175,10 @@ public struct Message: Identifiable, Hashable {
status: status,
createdAt: draft.createdAt,
isOutgoing: draft.isOutgoing,
isEditable: draft.isEditable,
isRetractable: draft.isRetractable,
isEdited: draft.isEdited,
isRetracted: draft.isRetracted,
dateReceived: draft.dateReceived,
address: draft.address,
isFirstMessage: draft.isFirstMessage,
@ -184,7 +200,7 @@ extension Message {
extension Message: Equatable {
public static func == (lhs: Message, rhs: Message) -> Bool {
lhs.id == rhs.id && lhs.status == rhs.status && lhs.isFirstMessage == rhs.isFirstMessage && lhs.ownReaction == rhs.ownReaction && lhs.reactions == rhs.reactions && lhs.ephemeralExpireTime == rhs.ephemeralExpireTime && lhs.attachments == rhs.attachments
lhs.id == rhs.id && lhs.status == rhs.status && lhs.isEdited == rhs.isEdited && lhs.isRetracted == rhs.isRetracted && lhs.isFirstMessage == rhs.isFirstMessage && lhs.text == rhs.text && lhs.attachments == rhs.attachments && lhs.replyMessage?.text == rhs.replyMessage?.text && lhs.replyMessage?.isRetracted == rhs.replyMessage?.isRetracted && lhs.ownReaction == rhs.ownReaction && lhs.reactions == rhs.reactions && lhs.ephemeralExpireTime == rhs.ephemeralExpireTime
}
}
@ -211,6 +227,10 @@ public struct ReplyMessage: Codable, Identifiable, Hashable {
public var isFirstMessage: Bool
public var text: String
public var isOutgoing: Bool
public var isEditable: Bool
public var isRetractable: Bool
public var isEdited: Bool
public var isRetracted: Bool
public var dateReceived: time_t
public var attachmentsNames: String
public var attachments: [Attachment]
@ -221,6 +241,10 @@ public struct ReplyMessage: Codable, Identifiable, Hashable {
isFirstMessage: Bool = false,
text: String = "",
isOutgoing: Bool,
isEditable: Bool,
isRetractable: Bool,
isEdited: Bool,
isRetracted: Bool,
dateReceived: time_t,
attachmentsNames: String = "",
attachments: [Attachment] = [],
@ -231,6 +255,10 @@ public struct ReplyMessage: Codable, Identifiable, Hashable {
self.isFirstMessage = isFirstMessage
self.text = text
self.isOutgoing = isOutgoing
self.isEditable = isEditable
self.isRetractable = isRetractable
self.isEdited = isEdited
self.isRetracted = isRetracted
self.dateReceived = dateReceived
self.attachmentsNames = attachmentsNames
self.attachments = attachments
@ -238,20 +266,24 @@ public struct ReplyMessage: Codable, Identifiable, Hashable {
}
func toMessage() -> Message {
Message(id: id, isOutgoing: isOutgoing, dateReceived: dateReceived, address: address, isFirstMessage: isFirstMessage, text: text, attachments: attachments, recording: recording)
Message(id: id, isOutgoing: isOutgoing, isEditable: isEditable, isRetractable: isRetractable, isEdited: isEdited, isRetracted: isRetracted, dateReceived: dateReceived, address: address, isFirstMessage: isFirstMessage, text: text, attachments: attachments, recording: recording)
}
}
public extension Message {
func toReplyMessage() -> ReplyMessage {
ReplyMessage(id: id, address: address, isFirstMessage: isFirstMessage, text: text, isOutgoing: isOutgoing, dateReceived: dateReceived, attachments: attachments, recording: recording)
ReplyMessage(id: id, address: address, isFirstMessage: isFirstMessage, text: text, isOutgoing: isOutgoing, isEditable: isEditable, isRetractable: isRetractable, isEdited: isEdited, isRetracted: isRetracted, dateReceived: dateReceived, attachments: attachments, recording: recording)
}
}
public struct DraftMessage {
public var id: String?
public let isOutgoing: Bool
public let isEditable: Bool
public let isRetractable: Bool
public let isEdited: Bool
public let isRetracted: Bool
public var dateReceived: time_t
public let address: String
public let isFirstMessage: Bool
@ -265,6 +297,10 @@ public struct DraftMessage {
public init(id: String? = nil,
isOutgoing: Bool,
isEditable: Bool,
isRetractable: Bool,
isEdited: Bool,
isRetracted: Bool,
dateReceived: time_t,
address: String,
isFirstMessage: Bool,
@ -278,6 +314,10 @@ public struct DraftMessage {
) {
self.id = id
self.isOutgoing = isOutgoing
self.isEditable = isEditable
self.isRetractable = isRetractable
self.isEdited = isEdited
self.isRetracted = isRetracted
self.dateReceived = dateReceived
self.address = address
self.isFirstMessage = isFirstMessage

View file

@ -93,6 +93,7 @@ class ConversationViewModel: ObservableObject {
@Published var selectedMessageToPlayVoiceRecording: EventLogMessage?
@Published var selectedMessage: EventLogMessage?
@Published var messageToReply: EventLogMessage?
@Published var messageToEdit: EventLogMessage?
@Published var sheetCategories: [SheetCategory] = []
@ -171,7 +172,152 @@ class ConversationViewModel: ObservableObject {
self.getEventMessage(eventLog: eventLog)
}, onEphemeralMessageDeleted: {(_: ChatRoom, eventLog: EventLog) in
self.removeMessage(eventLog)
}, onMessageContentEdited: {(chatRoom: ChatRoom, message: ChatMessage) in
let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLogId == message.messageId})
if let displayedConversation = self.sharedMainViewModel.displayedConversation {
displayedConversation.getContentTextMessage(chatRoom: displayedConversation.chatRoom)
}
var attachmentNameList: String = ""
var attachmentList: [Attachment] = []
var contentText = ""
if !message.contents.isEmpty {
message.contents.forEach { content in
if content.isText && content.name == nil {
contentText = content.utf8Text ?? ""
} else if content.name != nil && !content.name!.isEmpty {
if content.filePath == nil || content.filePath!.isEmpty {
let path = URL(string: self.getNewFilePath(name: content.name ?? ""))
if path != nil {
let attachment =
Attachment(
id: UUID().uuidString,
name: content.name!,
url: path!,
type: .fileTransfer,
size: content.fileSize,
transferProgressIndication: content.filePath != nil && !content.filePath!.isEmpty ? 100 : -1
)
attachmentNameList += ", \(content.name!)"
attachmentList.append(attachment)
}
} else {
if content.type != "video" {
let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/")
let path = URL(string: self.getNewFilePath(name: filePathSep[1]))
var typeTmp: AttachmentType = .other
switch content.type {
case "image":
typeTmp = (content.name?.lowercased().hasSuffix("gif"))! ? .gif : .image
case "audio":
typeTmp = content.isVoiceRecording ? .voiceRecording : .audio
case "application":
typeTmp = content.subtype.lowercased() == "pdf" ? .pdf : .other
case "text":
typeTmp = .text
default:
typeTmp = .other
}
if path != nil {
let attachment =
Attachment(
id: UUID().uuidString,
name: content.name!,
url: path!,
type: typeTmp,
duration: typeTmp == .voiceRecording ? content.fileDuration : 0,
size: content.fileSize,
transferProgressIndication: content.filePath != nil && !content.filePath!.isEmpty ? 100 : -1
)
attachmentNameList += ", \(content.name!)"
attachmentList.append(attachment)
if typeTmp != .voiceRecording {
DispatchQueue.main.async {
if !attachment.full.pathExtension.isEmpty {
self.attachments.append(attachment)
}
}
}
}
} else if content.type == "video" {
let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/")
let path = URL(string: self.getNewFilePath(name: filePathSep[1]))
let pathThumbnail = URL(string: self.generateThumbnail(name: filePathSep[1]))
if path != nil && pathThumbnail != nil {
let attachment =
Attachment(
id: UUID().uuidString,
name: content.name!,
thumbnail: pathThumbnail!,
full: path!,
type: .video,
size: content.fileSize,
transferProgressIndication: content.filePath != nil && !content.filePath!.isEmpty ? 100 : -1
)
attachmentNameList += ", \(content.name!)"
attachmentList.append(attachment)
DispatchQueue.main.async {
if !attachment.full.pathExtension.isEmpty {
self.attachments.append(attachment)
}
}
}
}
}
}
}
}
if !attachmentNameList.isEmpty {
attachmentNameList = String(attachmentNameList.dropFirst(2))
}
let indexReplyMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.replyMessage?.id == message.messageId})
DispatchQueue.main.async {
if indexMessage != nil {
self.conversationMessagesSection[0].rows[indexMessage!].message.text = contentText
self.conversationMessagesSection[0].rows[indexMessage!].message.isEdited = true
self.conversationMessagesSection[0].rows[indexMessage!].message.attachments = attachmentList
self.conversationMessagesSection[0].rows[indexMessage!].message.attachmentsNames = attachmentNameList
}
if indexReplyMessage != nil {
self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.text = contentText
}
}
}, onMessageRetracted: {(chatRoom: ChatRoom, message: ChatMessage) in
let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLogId == message.messageId})
let indexReplyMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.replyMessage?.id == message.messageId})
if let displayedConversation = self.sharedMainViewModel.displayedConversation {
displayedConversation.getContentTextMessage(chatRoom: displayedConversation.chatRoom)
}
DispatchQueue.main.async {
if indexMessage != nil {
self.conversationMessagesSection[0].rows[indexMessage!].message.text = ""
self.conversationMessagesSection[0].rows[indexMessage!].message.isRetracted = true
self.conversationMessagesSection[0].rows[indexMessage!].message.attachments = []
self.conversationMessagesSection[0].rows[indexMessage!].message.attachmentsNames = ""
}
if indexReplyMessage != nil {
self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.text = ""
self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.isRetracted = true
self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.attachments = []
self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.attachmentsNames = ""
}
}
})
self.chatRoomDelegateHolder = ChatRoomDelegateHolder(chatroom: chatRoom, delegate: chatRoomDelegate)
}
@ -220,6 +366,12 @@ class ConversationViewModel: ObservableObject {
self.coreContext.doOnCoreQueue { _ in
let chatMessageDelegate = ChatMessageDelegateStub(onMsgStateChanged: { (message: ChatMessage, msgState: ChatMessage.State) in
if msgState == .Queued || msgState == .PendingDelivery {
if let eventLog = message.eventLog {
self.getNewMessages(eventLogs: [eventLog])
}
return
}
var statusTmp: Message.Status?
switch message.state {
case .InProgress:
@ -324,12 +476,12 @@ class ConversationViewModel: ObservableObject {
if !self.conversationMessagesSection.isEmpty,
!self.conversationMessagesSection[0].rows.isEmpty {
let indexMessageEventLogId = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLogId.isEmpty && $0.eventModel.eventLog.chatMessage != nil ? $0.eventModel.eventLog.chatMessage!.messageId == message.messageId : false})
let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLogId == message.messageId})
DispatchQueue.main.async {
if let indexMessageEventLogId = indexMessageEventLogId, !self.conversationMessagesSection.isEmpty, !self.conversationMessagesSection[0].rows.isEmpty, self.conversationMessagesSection[0].rows.count > indexMessageEventLogId {
self.conversationMessagesSection[0].rows[indexMessageEventLogId].eventModel.eventLogId = message.messageId
}
let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLogId == message.messageId})
if let indexMessage = indexMessage, !self.conversationMessagesSection.isEmpty, !self.conversationMessagesSection[0].rows.isEmpty, self.conversationMessagesSection[0].rows.count > indexMessage {
self.conversationMessagesSection[0].rows[indexMessage].message.status = statusTmp ?? .error
}
@ -378,16 +530,21 @@ 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
}
}
})
self.chatMessageDelegateHolders.removeAll()
self.chatMessageDelegateHolders.append(ChatMessageDelegateHolder(message: message, delegate: chatMessageDelegate))
}
}
@ -541,6 +698,10 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString,
status: nil,
isOutgoing: false,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: false,
dateReceived: 0,
address: "",
isFirstMessage: false,
@ -701,6 +862,8 @@ class ConversationViewModel: ObservableObject {
let contentReplyText = chatMessage.replyMessage?.utf8Text ?? ""
let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false
var attachmentNameReplyList: String = ""
chatMessage.replyMessage?.contents.forEach { content in
@ -718,7 +881,11 @@ class ConversationViewModel: ObservableObject {
address: addressReplyCleaned?.asStringUriOnly() ?? "",
isFirstMessage: false,
text: contentReplyText,
isOutgoing: false,
isOutgoing: chatMessage.replyMessage!.isOutgoing,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: isReplyRetracted,
dateReceived: 0,
attachmentsNames: attachmentNameReplyList,
attachments: []
@ -732,6 +899,10 @@ class ConversationViewModel: ObservableObject {
id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString,
status: statusTmp,
isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false,
isEdited: chatMessage.isEdited,
isRetracted: chatMessage.isRetracted,
dateReceived: chatMessage.time,
address: addressCleaned?.asStringUriOnly() ?? "",
isFirstMessage: isFirstMessageTmp,
@ -785,6 +956,10 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString,
status: nil,
isOutgoing: false,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: false,
dateReceived: 0,
address: "",
isFirstMessage: false,
@ -944,6 +1119,8 @@ class ConversationViewModel: ObservableObject {
let contentReplyText = chatMessage.replyMessage?.utf8Text ?? ""
let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false
var attachmentNameReplyList: String = ""
chatMessage.replyMessage?.contents.forEach { content in
@ -961,7 +1138,11 @@ class ConversationViewModel: ObservableObject {
address: addressReplyCleaned?.asStringUriOnly() ?? "",
isFirstMessage: false,
text: contentReplyText,
isOutgoing: false,
isOutgoing: chatMessage.replyMessage!.isOutgoing,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: isReplyRetracted,
dateReceived: 0,
attachmentsNames: attachmentNameReplyList,
attachments: []
@ -975,6 +1156,10 @@ class ConversationViewModel: ObservableObject {
id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString,
status: statusTmp,
isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false,
isEdited: chatMessage.isEdited,
isRetracted: chatMessage.isRetracted,
dateReceived: chatMessage.time,
address: addressCleaned?.asStringUriOnly() ?? "",
isFirstMessage: isFirstMessageTmp,
@ -1045,6 +1230,10 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString,
status: nil,
isOutgoing: false,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: false,
dateReceived: 0,
address: "",
isFirstMessage: false,
@ -1218,6 +1407,8 @@ class ConversationViewModel: ObservableObject {
let contentReplyText = chatMessage.replyMessage?.utf8Text ?? ""
let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false
var attachmentNameReplyList: String = ""
chatMessage.replyMessage?.contents.forEach { content in
@ -1235,7 +1426,11 @@ class ConversationViewModel: ObservableObject {
address: addressReplyCleaned != nil ? addressReplyCleaned!.asStringUriOnly() : "",
isFirstMessage: false,
text: contentReplyText,
isOutgoing: false,
isOutgoing: chatMessage.replyMessage!.isOutgoing,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: isReplyRetracted,
dateReceived: 0,
attachmentsNames: attachmentNameReplyList,
attachments: []
@ -1250,6 +1445,10 @@ class ConversationViewModel: ObservableObject {
appData: chatMessage.appdata ?? "",
status: statusTmp,
isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false,
isEdited: chatMessage.isEdited,
isRetracted: chatMessage.isRetracted,
dateReceived: chatMessage.time,
address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "",
isFirstMessage: isFirstMessageTmp,
@ -1450,6 +1649,8 @@ class ConversationViewModel: ObservableObject {
let contentReplyText = chatMessage.replyMessage?.utf8Text ?? ""
let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false
var attachmentNameReplyList: String = ""
chatMessage.replyMessage?.contents.forEach { content in
@ -1467,7 +1668,11 @@ class ConversationViewModel: ObservableObject {
address: addressReplyCleaned != nil ? addressReplyCleaned!.asStringUriOnly() : "",
isFirstMessage: false,
text: contentReplyText,
isOutgoing: false,
isOutgoing: chatMessage.replyMessage!.isOutgoing,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: isReplyRetracted,
dateReceived: 0,
attachmentsNames: attachmentNameReplyList,
attachments: []
@ -1482,6 +1687,10 @@ class ConversationViewModel: ObservableObject {
appData: chatMessage.appdata ?? "",
status: statusTmp,
isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false,
isEdited: chatMessage.isEdited,
isRetracted: chatMessage.isRetracted,
dateReceived: chatMessage.time,
address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "",
isFirstMessage: isFirstMessageTmp,
@ -1523,6 +1732,10 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString,
status: nil,
isOutgoing: false,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: false,
dateReceived: 0,
address: "",
isFirstMessage: false,
@ -1550,6 +1763,9 @@ class ConversationViewModel: ObservableObject {
}
func replyToMessage(index: Int, isMessageTextFocused: Binding<Bool>) {
if self.messageToEdit != nil {
self.messageToEdit = nil
}
coreContext.doOnCoreQueue { _ in
let messageToReplyTmp = self.conversationMessagesSection[0].rows[index]
DispatchQueue.main.async {
@ -1561,6 +1777,21 @@ class ConversationViewModel: ObservableObject {
}
}
func editMessage(chatMessage: EventLogMessage, isMessageTextFocused: Binding<Bool>) {
if self.messageToReply != nil {
self.messageToReply = nil
}
coreContext.doOnCoreQueue { _ in
let messageToEditTmp = chatMessage
DispatchQueue.main.async {
withAnimation(.linear(duration: 0.15)) {
self.messageToEdit = messageToEditTmp
}
isMessageTextFocused.wrappedValue = true
}
}
}
func resendMessage(chatMessage: EventLogMessage) {
coreContext.doOnCoreQueue { _ in
if let message = chatMessage.eventModel.eventLog.chatMessage {
@ -1616,6 +1847,10 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString,
status: nil,
isOutgoing: false,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: false,
dateReceived: 0,
address: "",
isFirstMessage: false,
@ -1775,6 +2010,8 @@ class ConversationViewModel: ObservableObject {
let contentReplyText = chatMessage.replyMessage?.utf8Text ?? ""
let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false
var attachmentNameReplyList: String = ""
chatMessage.replyMessage?.contents.forEach { content in
@ -1792,7 +2029,11 @@ class ConversationViewModel: ObservableObject {
address: addressReplyCleaned?.asStringUriOnly() ?? "",
isFirstMessage: false,
text: contentReplyText,
isOutgoing: false,
isOutgoing: chatMessage.replyMessage!.isOutgoing,
isEditable: false,
isRetractable: false,
isEdited: false,
isRetracted: isReplyRetracted,
dateReceived: 0,
attachmentsNames: attachmentNameReplyList,
attachments: []
@ -1806,6 +2047,10 @@ class ConversationViewModel: ObservableObject {
id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString,
status: statusTmp,
isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false,
isEdited: chatMessage.isEdited,
isRetracted: chatMessage.isRetracted,
dateReceived: chatMessage.time,
address: addressCleaned?.asStringUriOnly() ?? "",
isFirstMessage: isFirstMessageTmp,
@ -1870,6 +2115,8 @@ class ConversationViewModel: ObservableObject {
if chatMessageToReply != nil {
message = try self.sharedMainViewModel.displayedConversation!.chatRoom.createReplyMessage(message: chatMessageToReply!)
}
} else if let chatMessage = self.messageToEdit?.eventModel.eventLog.chatMessage {
message = try self.sharedMainViewModel.displayedConversation!.chatRoom.createReplacesMessage(message: chatMessage)
} else {
message = try self.sharedMainViewModel.displayedConversation!.chatRoom.createEmptyMessage()
}
@ -1942,15 +2189,20 @@ class ConversationViewModel: ObservableObject {
}
}
if message != nil && !message!.contents.isEmpty {
if let message = message , !message.contents.isEmpty {
Log.info("[ConversationViewModel] Sending message")
message!.send()
self.addChatMessageDelegate(message: message)
message.send()
self.sharedMainViewModel.displayedConversation!.chatRoom.stopComposing()
}
Log.info("[ConversationViewModel] Message sent, re-setting defaults")
DispatchQueue.main.async {
self.messageToReply = nil
self.messageToEdit = nil
withAnimation {
self.mediasToSend.removeAll()
}
@ -2001,25 +2253,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)
}
}
}
@ -2277,7 +2527,7 @@ class ConversationViewModel: ObservableObject {
dispatchGroup.enter()
ContactAvatarModel.getAvatarModelFromAddress(address: chatMessageReaction.fromAddress!) { avatarResult in
if let account = core.defaultAccount,
let contactAddress = account.contactAddress,
let contactAddress = account.params?.identityAddress,
contactAddress.asStringUriOnly().contains(avatarResult.address) {
let innerSheetCat = InnerSheetCategory(
@ -2369,10 +2619,20 @@ class ConversationViewModel: ObservableObject {
}
}
func compose() {
func compose(stop: Bool, cachedConversation: ConversationModel? = nil) {
coreContext.doOnCoreQueue { _ in
if self.sharedMainViewModel.displayedConversation != nil {
self.sharedMainViewModel.displayedConversation!.chatRoom.compose()
if let displayedConversation = self.sharedMainViewModel.displayedConversation {
if stop {
displayedConversation.chatRoom.stopComposing()
} else {
displayedConversation.chatRoom.composeTextMessage()
}
} else if let displayedConversation = cachedConversation {
if stop {
displayedConversation.chatRoom.stopComposing()
} else {
displayedConversation.chatRoom.composeTextMessage()
}
}
}
}
@ -2671,17 +2931,39 @@ class ConversationViewModel: ObservableObject {
if let displayedConversation = self.sharedMainViewModel.displayedConversation,
let selectedMessage = self.selectedMessage,
let chatMessage = selectedMessage.eventModel.eventLog.chatMessage {
let indexReplyMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.replyMessage?.id == chatMessage.messageId})
displayedConversation.chatRoom.deleteMessage(message: chatMessage)
displayedConversation.getContentTextMessage(chatRoom: displayedConversation.chatRoom)
DispatchQueue.main.async {
if let sectionIndex = self.conversationMessagesSection.firstIndex(where: { $0.chatRoomID == displayedConversation.id }),
let rowIndex = self.conversationMessagesSection[sectionIndex].rows.firstIndex(of: selectedMessage) {
self.conversationMessagesSection[sectionIndex].rows.remove(at: rowIndex)
if indexReplyMessage != nil {
self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage = nil
}
}
self.selectedMessage = nil
}
}
}
}
func deleteMessageForEveryone(){
coreContext.doOnCoreQueue { _ in
if let displayedConversation = self.sharedMainViewModel.displayedConversation,
let selectedMessage = self.selectedMessage,
let chatMessage = selectedMessage.eventModel.eventLog.chatMessage {
displayedConversation.chatRoom.retractMessage(message: chatMessage)
DispatchQueue.main.async {
self.selectedMessage = nil
}
}
}
}
}
// swiftlint:enable line_length
// swiftlint:enable type_body_length
@ -2780,6 +3062,21 @@ class VoiceRecordPlayerManager {
}
}
func seekVoiceRecordPlayer(percent: Double) {
guard !isPlayerClosed(),
let player = voiceRecordPlayer,
player.duration > 0 else { return }
let clamped = max(0, min(percent, 100))
let ratio = clamped / 100.0
let timeMs = Int(Double(player.duration) * ratio)
print("Seek voice record to \(clamped)% (\(timeMs) ms)")
try? player.seek(timeMs: timeMs)
}
func getSpeakerSoundCard(core: Core) -> String? {
var speakerCard: String? = nil
var earpieceCard: String? = nil

View file

@ -89,16 +89,16 @@ class ConversationsListViewModel: ObservableObject {
fromAddressFriend = nil
}
let lastMessageTextTmp = (fromAddressFriend ?? "") + (lastMessage.contents.first(where: { $0.isText })?.utf8Text ?? (lastMessage.contents.first(where: { $0.isFile || $0.isFileTransfer })?.name ?? ""))
let lastMessagePrefixTextTmp = (fromAddressFriend ?? "")
if let index = self.conversationsList.firstIndex(where: { $0.chatRoom === conversationModel.chatRoom }) {
DispatchQueue.main.async {
conversationModel.lastMessageText = lastMessageTextTmp
self.conversationsList[index].lastMessageText = lastMessageTextTmp
conversationModel.lastMessagePrefixText = lastMessagePrefixTextTmp
self.conversationsList[index].lastMessagePrefixText = lastMessagePrefixTextTmp
}
} else {
DispatchQueue.main.async {
conversationModel.lastMessageText = lastMessageTextTmp
conversationModel.lastMessagePrefixText = lastMessagePrefixTextTmp
}
}
}
@ -148,16 +148,16 @@ class ConversationsListViewModel: ObservableObject {
fromAddressFriend = nil
}
let lastMessageTextTmp = (fromAddressFriend ?? "") + (lastMessage.contents.first(where: { $0.isText })?.utf8Text ?? (lastMessage.contents.first(where: { $0.isFile || $0.isFileTransfer })?.name ?? ""))
let lastMessagePrefixTextTmp = (fromAddressFriend ?? "")
if let index = self.conversationsList.firstIndex(where: { $0.chatRoom === conversationModel.chatRoom }) {
DispatchQueue.main.async {
conversationModel.lastMessageText = lastMessageTextTmp
self.conversationsList[index].lastMessageText = lastMessageTextTmp
conversationModel.lastMessagePrefixText = lastMessagePrefixTextTmp
self.conversationsList[index].lastMessagePrefixText = lastMessagePrefixTextTmp
}
} else {
DispatchQueue.main.async {
conversationModel.lastMessageText = lastMessageTextTmp
conversationModel.lastMessagePrefixText = lastMessagePrefixTextTmp
}
}
}
@ -183,69 +183,76 @@ class ConversationsListViewModel: ObservableObject {
func addConversationDelegate() {
coreContext.doOnCoreQueue { core in
self.coreConversationDelegate = CoreDelegateStub(onMessagesReceived: { (core: Core, chatRoom: ChatRoom, _: [ChatMessage]) in
if let defaultAddress = core.defaultAccount?.contactAddress,
let localAddress = chatRoom.localAddress,
defaultAddress.weakEqual(address2: localAddress) {
let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom)
let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom)
model.getContentTextMessage(chatRoom: chatRoom)
let index = self.conversationsList.firstIndex(where: { $0.id == idTmp })
DispatchQueue.main.async {
if index != nil {
self.conversationsList.remove(at: index!)
}
self.conversationsList.insert(model, at: 0)
}
SharedMainViewModel.shared.updateUnreadMessagesCount()
}
}, onMessageSent: { (_: Core, chatRoom: ChatRoom, _: ChatMessage) in
if let defaultAddress = core.defaultAccount?.contactAddress,
let localAddress = chatRoom.localAddress,
defaultAddress.weakEqual(address2: localAddress) {
let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom)
let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom)
model.getContentTextMessage(chatRoom: chatRoom)
let index = self.conversationsList.firstIndex(where: { $0.id == idTmp })
if index != nil {
self.conversationsList[index!].chatMessageRemoveDelegate()
}
DispatchQueue.main.async {
if index != nil {
self.conversationsList.remove(at: index!)
}
self.conversationsList.insert(model, at: 0)
}
SharedMainViewModel.shared.updateUnreadMessagesCount()
}
}, onChatRoomRead: { (_: Core, chatRoom: ChatRoom) in
let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom)
let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom)
model.getContentTextMessage(chatRoom: chatRoom)
if let index = self.conversationsList.firstIndex(where: { $0.id == idTmp }) {
DispatchQueue.main.async {
self.conversationsList.remove(at: index)
self.conversationsList.insert(model, at: index)
self.coreConversationDelegate = CoreDelegateStub(
onMessagesReceived: { (core: Core, chatRoom: ChatRoom, _: [ChatMessage]) in
if let defaultAddress = core.defaultAccount?.params?.identityAddress,
let localAddress = chatRoom.localAddress,
defaultAddress.weakEqual(address2: localAddress) {
let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom)
let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom)
model.getContentTextMessage(chatRoom: chatRoom)
let index = self.conversationsList.firstIndex(where: { $0.id == idTmp })
DispatchQueue.main.async {
if index != nil {
self.conversationsList.remove(at: index!)
}
self.conversationsList.insert(model, at: 0)
}
SharedMainViewModel.shared.updateUnreadMessagesCount()
}
}, onMessageSent: { (_: Core, chatRoom: ChatRoom, _: ChatMessage) in
if let defaultAddress = core.defaultAccount?.params?.identityAddress,
let localAddress = chatRoom.localAddress,
defaultAddress.weakEqual(address2: localAddress) {
let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom)
let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom)
model.getContentTextMessage(chatRoom: chatRoom)
let index = self.conversationsList.firstIndex(where: { $0.id == idTmp })
if index != nil {
self.conversationsList[index!].chatMessageRemoveDelegate()
}
DispatchQueue.main.async {
if index != nil {
self.conversationsList.remove(at: index!)
}
self.conversationsList.insert(model, at: 0)
}
SharedMainViewModel.shared.updateUnreadMessagesCount()
}
}, onChatRoomRead: { (_: Core, chatRoom: ChatRoom) in
let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom)
let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom)
model.getContentTextMessage(chatRoom: chatRoom)
if let index = self.conversationsList.firstIndex(where: { $0.id == idTmp }) {
DispatchQueue.main.async {
self.conversationsList.remove(at: index)
self.conversationsList.insert(model, at: index)
}
}
SharedMainViewModel.shared.updateUnreadMessagesCount()
}, onChatRoomStateChanged: { (core: Core, chatroom: ChatRoom, state: ChatRoom.State) in
// Log.info("[ConversationsListViewModel] Conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]")
if let defaultAddress = core.defaultAccount?.params?.identityAddress,
let localAddress = chatroom.localAddress,
defaultAddress.weakEqual(address2: localAddress) {
if core.globalState == .On {
switch state {
case .Created:
self.addChatRoom(chatRoom: chatroom)
case .Deleted:
self.removeChatRoom(chatRoom: chatroom)
default:
break
}
}
}
}, onMessageRetracted: { (core: Core, chatRoom: ChatRoom, message: ChatMessage) in
let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom)
let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom)
model.getContentTextMessage(chatRoom: chatRoom)
SharedMainViewModel.shared.updateUnreadMessagesCount()
}
SharedMainViewModel.shared.updateUnreadMessagesCount()
}, onChatRoomStateChanged: { (core: Core, chatroom: ChatRoom, state: ChatRoom.State) in
// Log.info("[ConversationsListViewModel] Conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]")
if let defaultAddress = core.defaultAccount?.contactAddress,
let localAddress = chatroom.localAddress,
defaultAddress.weakEqual(address2: localAddress) {
if core.globalState == .On {
switch state {
case .Created:
self.addChatRoom(chatRoom: chatroom)
case .Deleted:
self.removeChatRoom(chatRoom: chatroom)
default:
break
}
}
}
})
)
core.addDelegate(delegate: self.coreConversationDelegate!)
}
}
@ -450,25 +457,25 @@ class ConversationsListViewModel: ObservableObject {
func changeDisplayedChatRoom(conversationModel: ConversationModel) {
CoreContext.shared.doOnCoreQueue { core in
let nilParams: ConferenceParams? = nil
if let newChatRoom = core.searchChatRoom(params: nilParams, localAddr: nil, remoteAddr: conversationModel.chatRoom.peerAddress, participants: nil) {
if LinphoneUtils.getChatRoomId(room: newChatRoom) == conversationModel.id {
if self.sharedMainViewModel.displayedConversation == nil {
DispatchQueue.main.async {
withAnimation {
self.sharedMainViewModel.displayedConversation = conversationModel
}
}
} else {
DispatchQueue.main.async {
self.sharedMainViewModel.displayedConversation = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation {
self.sharedMainViewModel.displayedConversation = conversationModel
}
}
}
}
if let newChatRoom = core.searchChatRoomByIdentifier(identifier: conversationModel.id) {
if self.sharedMainViewModel.displayedConversation == nil {
DispatchQueue.main.async {
withAnimation {
self.sharedMainViewModel.displayedConversation = conversationModel
}
}
} else {
DispatchQueue.main.async {
self.sharedMainViewModel.displayedConversation = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation {
self.sharedMainViewModel.displayedConversation = conversationModel
}
}
}
}
} else {
Log.warn("\(ConversationsListViewModel.TAG) changeDisplayedChatRoom: no chat room found for identifier \(conversationModel.id)")
}
}
}

View file

@ -86,13 +86,3 @@ final class CustomHostingController<Content: View>: UIHostingController<Content>
}
}
}
public struct LazyView<Content: View>: View {
private let build: () -> Content
public init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
public var body: Content {
build()
}
}

View file

@ -105,7 +105,7 @@ struct PopupUpdatePassword: View {
updateAuthInfo()
isShowUpdatePasswordPopup = false
}, label: {
Text("dialog_ok")
Text("dialog_confirm")
.default_text_style_white_600(styleSize: 20)
.frame(height: 35)
.frame(maxWidth: .infinity)

View file

@ -34,6 +34,9 @@ struct PopupView: View {
var titleSecondButton: Text?
var actionSecondButton: () -> Void
var titleThirdButton: Text?
var actionThirdButton: () -> Void
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
@ -49,40 +52,57 @@ struct PopupView: View {
.padding(.bottom, 20)
}
if titleFirstButton != nil {
Button(action: {
actionFirstButton()
}, label: {
titleFirstButton
.default_text_style_orange_600(styleSize: 20)
.frame(height: 35)
.frame(maxWidth: .infinity)
})
.padding(.horizontal, 20)
.padding(.vertical, 10)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(Color.orangeMain500, lineWidth: 1)
)
.padding(.bottom, 10)
}
if titleSecondButton != nil {
Button(action: {
actionSecondButton()
}, label: {
titleSecondButton
.default_text_style_white_600(styleSize: 20)
.frame(height: 35)
.frame(maxWidth: .infinity)
})
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.orangeMain500)
.cornerRadius(60)
HStack {
if titleFirstButton != nil {
Button(action: {
actionFirstButton()
}, label: {
titleFirstButton
.default_text_style_white_600(styleSize: 14)
.frame(height: 30)
})
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.orangeMain500)
.cornerRadius(60)
.padding(.horizontal, 2)
}
if titleSecondButton != nil {
Button(action: {
actionSecondButton()
}, label: {
titleSecondButton
.default_text_style_white_600(styleSize: 14)
.frame(height: 30)
})
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.orangeMain500)
.cornerRadius(60)
.padding(.horizontal, 2)
}
if titleThirdButton != nil {
Button(action: {
actionThirdButton()
}, label: {
titleThirdButton
.default_text_style_orange_600(styleSize: 14)
.frame(height: 30)
})
.padding(.horizontal, 20)
.padding(.vertical, 10)
.cornerRadius(60)
.overlay(
RoundedRectangle(cornerRadius: 60)
.inset(by: 0.5)
.stroke(Color.orangeMain500, lineWidth: 1)
)
.padding(.horizontal, 2)
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(.horizontal, 20)
.padding(.vertical, 20)
@ -101,9 +121,11 @@ struct PopupView: View {
PopupView(isShowPopup: .constant(true),
title: Text("Title"),
content: Text("Content"),
titleFirstButton: Text("Deny all"),
titleFirstButton: Text("Accept all"),
actionFirstButton: {},
titleSecondButton: Text("Accept all"),
actionSecondButton: {})
titleSecondButton: Text("dialog_confirm"),
actionSecondButton: {},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: {})
.background(.black.opacity(0.65))
}

View file

@ -72,7 +72,7 @@ struct PopupViewWithTextField: View {
setNewChatRoomSubject()
isShowConversationInfoPopup = false
}, label: {
Text("dialog_ok")
Text("dialog_confirm")
.default_text_style_white_600(styleSize: 20)
.frame(height: 35)
.frame(maxWidth: .infinity)

View file

@ -32,6 +32,7 @@ struct SideMenu: View {
@Binding var isShowLoginFragment: Bool
@Binding var isShowAccountProfileFragment: Bool
@Binding var isShowSettingsFragment: Bool
@Binding var isShowRecordingsListFragment: Bool
@Binding var isShowHelpFragment: Bool
@State private var showHelp = false
@ -137,12 +138,15 @@ struct SideMenu: View {
}
}
/*
SideMenuEntry(
iconName: "record-fill",
title: "recordings_title"
)
*/
).onTapGesture {
self.menuClose()
withAnimation {
isShowRecordingsListFragment = true
}
}
SideMenuEntry(
iconName: "question",
@ -152,7 +156,6 @@ struct SideMenu: View {
withAnimation {
isShowHelpFragment = true
}
}
}
.padding(.bottom, safeAreaInsets.bottom + 13)
@ -176,15 +179,15 @@ struct SideMenu: View {
#Preview {
GeometryReader { geometry in
@State var triggerNavigateToLogin: Bool = false
SideMenu(
width: geometry.size.width / 5 * 4,
isOpen: .constant(true),
menuClose: {},
safeAreaInsets: geometry.safeAreaInsets,
isShowLoginFragment: $triggerNavigateToLogin,
isShowLoginFragment: .constant(false),
isShowAccountProfileFragment: .constant(false),
isShowSettingsFragment: .constant(false),
isShowRecordingsListFragment: .constant(false),
isShowHelpFragment: .constant(false)
)
.ignoresSafeArea(.all)

View file

@ -90,15 +90,51 @@ struct SideMenuAccountRow: View {
Spacer()
HStack {
if model.voicemailCount > 0 {
Button {
model.callVoicemailUri()
} label: {
ZStack(alignment: .top) {
VStack {
Spacer()
Image("voicemail")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 22, height: 22)
Spacer()
}
Text(String(model.voicemailCount))
.foregroundStyle(Color.redDanger500)
.default_text_style_600(styleSize: 12)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.top, 1)
}
.frame(width: 30, height: 30)
}
.highPriorityGesture(
TapGesture().onEnded {
model.callVoicemailUri()
}
)
}
if model.notificationsCount > 0 && !CorePreferences.disableChatFeature {
Text(String(model.notificationsCount))
.foregroundStyle(.white)
.default_text_style(styleSize: 12)
.lineLimit(1)
.frame(width: 20, height: 20)
.background(Color.redDanger500)
.cornerRadius(50)
.frame(maxWidth: .infinity, alignment: .leading)
VStack {
Text(String(model.notificationsCount))
.foregroundStyle(.white)
.default_text_style(styleSize: 12)
.lineLimit(1)
.frame(width: 20, height: 20)
.background(Color.redDanger500)
.cornerRadius(50)
}
.frame(width: 30, height: 30)
.padding(.trailing, -8)
}
Menu {
@ -112,15 +148,18 @@ struct SideMenuAccountRow: View {
Label("drawer_menu_manage_account", systemImage: "arrow.right.circle")
}
} label: {
Image("dots-three-vertical")
.renderingMode(.template)
.resizable()
.foregroundColor(Color.gray)
.scaledToFit()
.frame(height: 30)
VStack {
Image("dots-three-vertical")
.renderingMode(.template)
.resizable()
.foregroundColor(Color.grayMain2c500)
.scaledToFit()
.frame(height: 25)
}
.frame(width: 30, height: 30)
}
}
.frame(width: 64, alignment: .trailing)
.frame(alignment: .trailing)
.padding(.top, 12)
.padding(.bottom, 12)
}

View file

@ -315,6 +315,27 @@ struct ToastView: View {
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 15)
.padding(8)
case "Success_settings_contacts_carddav_sync_successful_toast":
Text("settings_contacts_carddav_sync_successful_toast")
.multilineTextAlignment(.center)
.foregroundStyle(Color.greenSuccess500)
.default_text_style(styleSize: 15)
.padding(8)
case "settings_contacts_carddav_sync_error_toast":
Text("settings_contacts_carddav_sync_error_toast")
.multilineTextAlignment(.center)
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 15)
.padding(8)
case "Success_settings_contacts_carddav_deleted_toast":
Text("settings_contacts_carddav_deleted_toast")
.multilineTextAlignment(.center)
.foregroundStyle(Color.greenSuccess500)
.default_text_style(styleSize: 15)
.padding(8)
default:
Text("Error")

View file

@ -25,18 +25,21 @@ struct HelpFragment: View {
@Binding var isShowHelpFragment: Bool
@State var advancedSettingsIsOpen: Bool = false
@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")
@ -70,11 +73,31 @@ struct HelpFragment: View {
ScrollView {
VStack(spacing: 0) {
VStack(spacing: 20) {
if let urlString = CorePreferences.themeAboutPictureUrl,
let url = URL(string: urlString) {
AsyncImage(url: url) { phase in
switch phase {
case .empty:
ProgressView()
.frame(maxWidth: .infinity, minHeight: 100, maxHeight: 100)
case .success(let image):
image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity, maxHeight: 100, alignment: .center)
case .failure:
EmptyView()
@unknown default:
EmptyView()
}
}
} else {
EmptyView()
}
Text("help_about_title")
.default_text_style_800(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 5)
Button {
if let url = URL(string: NSLocalizedString("website_user_guide_url", comment: "")) {
UIApplication.shared.open(url)
@ -154,7 +177,7 @@ struct HelpFragment: View {
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
Text(helpViewModel.version)
Text(helpViewModel.appVersion)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
@ -279,17 +302,17 @@ struct HelpFragment: View {
isShowPopup: $helpViewModel.checkUpdateAvailable,
title: Text("help_dialog_update_available_title"),
content: Text(String(format: String(localized: "help_dialog_update_available_message"), helpViewModel.versionAvailable)),
titleFirstButton: Text("dialog_cancel"),
actionFirstButton: {
helpViewModel.checkUpdateAvailable = false
},
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_install"),
actionSecondButton: {
helpViewModel.checkUpdateAvailable = false
if let url = URL(string: helpViewModel.urlVersionAvailable) {
UIApplication.shared.open(url)
}
}
},
titleThirdButton: Text("dialog_cancel"),
actionThirdButton: { helpViewModel.checkUpdateAvailable = false }
)
.background(.black.opacity(0.65))
.zIndex(3)
@ -302,5 +325,7 @@ struct HelpFragment: View {
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle("")
.navigationBarHidden(true)
}
}

View file

@ -38,13 +38,20 @@ class HelpViewModel: ObservableObject {
private var coreDelegate: CoreDelegate?
init() {
let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String
let versionTmp = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
let appGitVersion = AppGitInfo.commit
let appGitBranch = AppGitInfo.branch
let appGitTag = AppGitInfo.tag
let sdkGitVersion = linphonesw.sdkVersion
var sdkGitBranch = linphonesw.sdkBranch
self.version = (versionTmp ?? "6.0.0")
if sdkGitBranch.hasPrefix("remotes/origin/") {
sdkGitBranch = String(sdkGitBranch.dropFirst("remotes/origin/".count))
}
self.sdkVersion = Core.getVersion
self.appVersion = appGitTag
self.version = appGitTag + "-" + appGitVersion + "\n(\(appGitBranch))"
self.sdkVersion = sdkGitVersion + "\n(\(sdkGitBranch))"
if let path = Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist"),
let plist = NSDictionary(contentsOfFile: path) as? [String: Any],

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

@ -70,7 +70,10 @@ struct HistoryContactFragment: View {
Spacer()
Menu {
if !historyModel.isConf {
let disableAddContact = CorePreferences.disableAddContact
let isFriend = historyModel.isFriend == true
if !historyModel.isConf && (!disableAddContact || (disableAddContact && isFriend)) {
Button {
isMenuOpen = false
@ -187,21 +190,17 @@ struct HistoryContactFragment: View {
.frame(maxWidth: .infinity)
.padding(.top, 10)
Text(historyModel.address)
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity)
.padding(.top, 5)
if let avatarModel = historyModel.avatarModel {
Text(avatarModel.lastPresenceInfo)
.foregroundStyle(avatarModel.lastPresenceInfo == "Online" ? Color.greenSuccess500 : Color.orangeWarning600)
if !CorePreferences.hideSipAddresses {
Text(historyModel.address)
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style_300(styleSize: 12)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity)
.frame(height: 20)
.padding(.top, 5)
}
if let avatar = historyModel.avatarModel {
AvatarPresenceView(avatarModel: avatar)
} else {
Text("")
.multilineTextAlignment(.center)
@ -284,29 +283,31 @@ struct HistoryContactFragment: View {
}
})
}
Spacer()
Button(action: {
telecomManager.doCallOrJoinConf(address: historyModel.addressLinphone, isVideo: true)
}, label: {
VStack {
HStack(alignment: .center) {
Image("video-camera")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
if !SharedMainViewModel.shared.disableVideoCall {
Spacer()
Button(action: {
telecomManager.doCallOrJoinConf(address: historyModel.addressLinphone, isVideo: true)
}, label: {
VStack {
HStack(alignment: .center) {
Image("video-camera")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
}
.padding(16)
.background(Color.grayMain2c200)
.cornerRadius(40)
Text("contact_video_call_action")
.default_text_style(styleSize: 14)
.frame(minWidth: 80)
}
.padding(16)
.background(Color.grayMain2c200)
.cornerRadius(40)
Text("contact_video_call_action")
.default_text_style(styleSize: 14)
.frame(minWidth: 80)
}
})
})
}
} else {
Button(action: {
withAnimation {
@ -421,6 +422,21 @@ struct HistoryContactFragment: View {
}
}
struct AvatarPresenceView: View {
@ObservedObject var avatarModel: ContactAvatarModel
var body: some View {
Text(avatarModel.lastPresenceInfo)
.foregroundStyle(avatarModel.lastPresenceInfo == "Online" ? Color.greenSuccess500 : Color.orangeWarning600)
.multilineTextAlignment(.center)
.default_text_style_300(styleSize: 12)
.frame(maxWidth: .infinity)
.frame(height: 20)
.padding(.top, 5)
}
}
#Preview {
HistoryContactFragment(
isShowDeleteAllHistoryPopup: .constant(false),

View file

@ -39,7 +39,7 @@ struct HistoryFragment: View {
isShowEditContactFragment: $isShowEditContactFragment,
isShowEditContactFragmentAddress: $isShowEditContactFragmentAddress
)
.presentationDetents([.fraction(0.3)])
.presentationDetents([.fraction(0.4)])
}
} else {
HistoryListFragment(showingSheet: $showingSheet, text: $text)

View file

@ -58,68 +58,74 @@ struct HistoryListBottomSheet: View {
}
Spacer()
Button {
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
let disableAddContact = CorePreferences.disableAddContact
let isFriend = historyListViewModel.selectedCall?.isFriend == true
if !disableAddContact || (disableAddContact && isFriend) {
Button {
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
sharedMainViewModel.changeIndexView(indexViewInt: 0)
if let selectedCall = historyListViewModel.selectedCall, selectedCall.isFriend {
let friendIndex = contactsManager.avatarListModel.first(where: {$0.addresses.contains(where: {$0 == selectedCall.address})})
if friendIndex != nil {
sharedMainViewModel.changeIndexView(indexViewInt: 0)
if let selectedCall = historyListViewModel.selectedCall, selectedCall.isFriend {
let friendIndex = contactsManager.avatarListModel.first(where: {$0.addresses.contains(where: {$0 == selectedCall.address})})
if friendIndex != nil {
withAnimation {
SharedMainViewModel.shared.displayedFriend = friendIndex
}
}
} else if let selectedCall = historyListViewModel.selectedCall {
withAnimation {
SharedMainViewModel.shared.displayedFriend = friendIndex
isShowEditContactFragment.toggle()
isShowEditContactFragmentAddress = String(selectedCall.address.dropFirst(4))
}
}
} else if let selectedCall = historyListViewModel.selectedCall {
withAnimation {
isShowEditContactFragment.toggle()
isShowEditContactFragmentAddress = String(selectedCall.address.dropFirst(4))
} label: {
HStack {
if let selectedCall = historyListViewModel.selectedCall, selectedCall.isFriend {
Image("user-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("menu_see_existing_contact")
.default_text_style(styleSize: 16)
Spacer()
} else {
Image("plus-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("menu_add_address_to_contacts")
.default_text_style(styleSize: 16)
Spacer()
}
}
.frame(maxHeight: .infinity)
}
} label: {
HStack {
if let selectedCall = historyListViewModel.selectedCall, selectedCall.isFriend {
Image("user-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("menu_see_existing_contact")
.default_text_style(styleSize: 16)
Spacer()
} else {
Image("plus-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("menu_add_address_to_contacts")
.default_text_style(styleSize: 16)
Spacer()
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxHeight: .infinity)
.frame(maxWidth: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
if historyListViewModel.selectedCall != nil && historyListViewModel.selectedCall!.isOutgoing {

View file

@ -152,8 +152,10 @@ struct HistoryRow: View {
if !historyModel.isConf {
Image("phone")
.renderingMode(.template)
.resizable()
.frame(width: 25, height: 25)
.foregroundStyle(Color.grayMain2c600)
.padding(.all, 10)
.padding(.trailing, 5)
.highPriorityGesture(

View file

@ -235,75 +235,83 @@ struct StartCallFragment: View {
)
}
ScrollView {
if !ContactsManager.shared.lastSearch.isEmpty {
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
ZStack {
ScrollView {
if !ContactsManager.shared.lastSearch.isEmpty {
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
}
ContactsListFragment(showingSheet: .constant(false)
, startCallFunc: { addr in
if callViewModel.isTransferInsteadCall {
showingDialer = false
startCallViewModel.searchField = ""
magicSearch.currentFilter = ""
magicSearch.searchForContacts()
if callViewModel.isTransferInsteadCall == true {
callViewModel.isTransferInsteadCall = false
ContactsListFragment(showingSheet: .constant(false)
, startCallFunc: { addr in
if callViewModel.isTransferInsteadCall {
showingDialer = false
startCallViewModel.searchField = ""
magicSearch.currentFilter = ""
magicSearch.searchForContacts()
if callViewModel.isTransferInsteadCall == true {
callViewModel.isTransferInsteadCall = false
}
resetCallView()
delayColorDismiss()
withAnimation {
isShowStartCallFragment.toggle()
callViewModel.blindTransferCallTo(toAddress: addr)
}
} else {
showingDialer = false
startCallViewModel.searchField = ""
magicSearch.currentFilter = ""
magicSearch.searchForContacts()
if callViewModel.isTransferInsteadCall == true {
callViewModel.isTransferInsteadCall = false
}
resetCallView()
delayColorDismiss()
withAnimation {
isShowStartCallFragment.toggle()
telecomManager.doCallOrJoinConf(address: addr)
}
}
resetCallView()
delayColorDismiss()
withAnimation {
isShowStartCallFragment.toggle()
callViewModel.blindTransferCallTo(toAddress: addr)
}
} else {
showingDialer = false
startCallViewModel.searchField = ""
magicSearch.currentFilter = ""
magicSearch.searchForContacts()
if callViewModel.isTransferInsteadCall == true {
callViewModel.isTransferInsteadCall = false
}
resetCallView()
delayColorDismiss()
withAnimation {
isShowStartCallFragment.toggle()
telecomManager.doCallOrJoinConf(address: addr)
}
}
})
.padding(.horizontal, 16)
if !contactsManager.lastSearchSuggestions.isEmpty {
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
})
.padding(.horizontal, 16)
suggestionsList
if !contactsManager.lastSearchSuggestions.isEmpty {
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
suggestionsList
}
}
if magicSearch.isLoading {
ProgressView()
.controlSize(.large)
.progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500))
}
}
}
@ -385,6 +393,7 @@ struct StartCallFragment: View {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil {
if contactsManager.lastSearchSuggestions[index].address!.domain != CorePreferences.defaultDomain {
Image(uiImage: contactsManager.textToImage(
firstName: String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)),
lastName: ""))
@ -394,9 +403,29 @@ struct StartCallFragment: View {
Text(String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)))
.default_text_style(styleSize: 16)
.lineLimit(1)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
} else {
if let address = contactsManager.lastSearchSuggestions[index].address {
let nameTmp = address.displayName
?? address.username
?? String(address.asStringUriOnly().dropFirst(4))
Image(uiImage: contactsManager.textToImage(
firstName: nameTmp,
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(nameTmp)
.default_text_style(styleSize: 16)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
} else {
Image("profil-picture-default")
.resizable()

View file

@ -45,7 +45,12 @@ class HistoryListViewModel: ObservableObject {
func computeCallLogsList() {
coreContext.doOnCoreQueue { core in
let account = core.defaultAccount
let logs = account?.callLogs != nil ? account!.callLogs : core.callLogs
// Fetch all call logs if only one account to workaround no history issue
// TODO FIXME: remove workaround later
let logs = (core.accountList.count > 1)
? (account?.callLogs ?? core.callLogs)
: core.callLogs
var callLogsBis: [HistoryModel] = []
var callLogsTmpBis: [HistoryModel] = []

View file

@ -157,7 +157,7 @@ class StartCallViewModel: ObservableObject {
func interpretAndStartCall() {
CoreContext.shared.doOnCoreQueue { core in
let address = core.interpretUrl(url: self.searchField, applyInternationalPrefix: true)
let address = core.interpretUrl(url: self.searchField, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core))
if address != nil {
TelecomManager.shared.doCallOrJoinConf(address: address!)
}

View file

@ -167,57 +167,47 @@ struct AddParticipantsFragment: View {
.padding(.bottom)
.padding(.horizontal)
ScrollView {
ForEach(0..<contactsManager.lastSearch.count, id: \.self) { index in
HStack {
ZStack {
ScrollView {
ForEach(0..<contactsManager.avatarListModel.count, id: \.self) { index in
HStack {
if index == 0
|| contactsManager.lastSearch[index].friend?.name!.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first
!= contactsManager.lastSearch[index-1].friend?.name!.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first {
Text(
String(
(contactsManager.lastSearch[index].friend?.name!.uppercased().folding(
options: .diacriticInsensitive,
locale: .current
).first)!))
.contact_text_style_500(styleSize: 20)
.frame(width: 18)
.padding(.leading, 5)
.padding(.trailing, 5)
} else {
Text("")
HStack {
if index == 0
|| contactsManager.avatarListModel[index].name.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first
!= contactsManager.avatarListModel[index-1].name.lowercased().folding(
options: .diacriticInsensitive,
locale: .current
).first {
Text(
String(
(contactsManager.avatarListModel[index].name.uppercased().folding(
options: .diacriticInsensitive,
locale: .current
).first)!))
.contact_text_style_500(styleSize: 20)
.frame(width: 18)
.padding(.leading, 5)
.padding(.trailing, 5)
}
if index < contactsManager.avatarListModel.count,
let friend = contactsManager.avatarListModel[index].friend,
let photo = friend.photo,
!photo.isEmpty {
} else {
Text("")
.contact_text_style_500(styleSize: 20)
.frame(width: 18)
.padding(.leading, 5)
.padding(.trailing, 5)
}
Avatar(contactAvatarModel: contactsManager.avatarListModel[index], avatarSize: 50)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 50, height: 50)
.clipShape(Circle())
}
Text((contactsManager.lastSearch[index].friend?.name ?? "")!)
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
if let searchAddress = contactsManager.lastSearch[index].friend?.address?.asStringUriOnly() {
Text(contactsManager.avatarListModel[index].name)
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
if addParticipantsViewModel.participantsToAdd.contains(where: {
$0.address.asStringUriOnly() == searchAddress
$0.address.asStringUriOnly() == contactsManager.avatarListModel[index].address
}) {
Image("check")
.renderingMode(.template)
@ -228,27 +218,33 @@ struct AddParticipantsFragment: View {
}
}
}
}
.background(.white)
.onTapGesture {
if let addr = contactsManager.lastSearch[index].address {
addParticipantsViewModel.selectParticipant(addr: addr)
.background(.white)
.onTapGesture {
if let addr = try? Factory.Instance.createAddress(addr: contactsManager.avatarListModel[index].address) {
addParticipantsViewModel.selectParticipant(addr: addr)
}
}
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
suggestionsList
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
suggestionsList
if magicSearch.isLoading {
ProgressView()
.controlSize(.large)
.progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500))
}
}
}
Button {
@ -294,19 +290,39 @@ struct AddParticipantsFragment: View {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil {
Image(uiImage: contactsManager.textToImage(
firstName: String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)),
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)))
.default_text_style(styleSize: 16)
.lineLimit(1)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
if contactsManager.lastSearchSuggestions[index].address!.domain != CorePreferences.defaultDomain {
Image(uiImage: contactsManager.textToImage(
firstName: String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)),
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)))
.default_text_style(styleSize: 16)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
} else {
if let address = contactsManager.lastSearchSuggestions[index].address {
let nameTmp = address.displayName
?? address.username
?? String(address.asStringUriOnly().dropFirst(4))
Image(uiImage: contactsManager.textToImage(
firstName: nameTmp,
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(nameTmp)
.default_text_style(styleSize: 16)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
if let searchAddress = contactsManager.lastSearchSuggestions[index].address?.asStringUriOnly() {
if addParticipantsViewModel.participantsToAdd.contains(where: {

View file

@ -56,7 +56,7 @@ struct MeetingsListBottomSheet: View {
Button {
CoreContext.shared.doOnCoreQueue { core in
if let organizerUri = self.meetingsListViewModel.selectedMeetingToDelete?.confInfo.organizer {
if core.defaultAccount?.contactAddress?.weakEqual(address2: organizerUri) ?? false {
if core.defaultAccount?.params?.identityAddress?.weakEqual(address2: organizerUri) ?? false {
// If we are the organizer, display popup for sending
DispatchQueue.main.async {
self.isShowSendCancelMeetingNotificationPopup = true

View file

@ -468,7 +468,7 @@ struct ScheduleMeetingFragment: View {
showDatePicker.toggle()
}
}
Text("dialog_ok")
Text("dialog_confirm")
.default_text_style_orange_500(styleSize: 16)
.onTapGesture {
pickDate()

Some files were not shown because too many files have changed in this diff Show more