Compare commits

...

191 commits

Author SHA1 Message Date
Christophe Deschamps
156231f8aa Handle VoIP and APNs push received before shared filesystem is available (BFU)
Delay CoreContext initialization until AppServices.config is available.
      When a VoIP push arrives before that, an EarlyPushkitDelegate reports
      a temporary CallKit call, ends it as unanswered after 4s, and posts
      a missed call notification to the user.
      Handle message APNs in service extension without Core/Config availability
2026-04-13 13:38:39 +00:00
Sylvain Berfini
0132c5253f Updated translations from Weblate 2026-03-26 11:56:11 +01:00
Benoit Martins
828fadb9e0 Merge branch 'master'
#Conflicts:
#	Linphone/GeneratedGit.swift
2026-03-26 10:27:22 +01:00
Benoit Martins
9524c6f2f3 Add a screen-sharing video preview to the call view 2026-03-26 10:23:36 +01:00
Benoit Martins
8a612be0a0 Fix phoneListsEqual to return false when both lists are empty 2026-03-26 08:47:00 +01:00
Benoit Martins
127e12b384 Add end-to-end encryption check for conference 2026-03-24 14:41:15 +01:00
Benoit Martins
b84bd1faf3 Add SipAddressesPopup to StartCallFragment, StartConversationFragment, and AddParticipantsFragment 2026-03-23 16:27:31 +01:00
Christophe Deschamps
54b8ae4b02 RC option to hide contact edition - ui/hide_contact_edition 2026-03-19 13:01:11 +00:00
Benoit Martins
5615d51e27 Added new colors to replace old ones, reworked themes to only use two "main" colors 2026-03-18 17:15:12 +00:00
Christophe Deschamps
fed6394cd5 Option ui/only_allow_earpiece_during_call to enforce the use or earpiecee only during call 2026-03-18 15:36:37 +00:00
Benoit Martins
b82156d2f2 Fix document preview 2026-03-18 15:50:21 +01:00
Benoit Martins
83acef02d7 Add deleted message bottom sheet when message is not retractable 2026-03-18 15:30:13 +01:00
Benoit Martins
068d2d2902 Fix media encryption state in CallView 2026-03-18 15:05:16 +01:00
Benoit Martins
1943f0f21c Fix mediaEncryption setting and media list for friends with multiple addresses 2026-03-17 16:16:40 +01:00
Benoit Martins
1ffcd3e1ae Add bottom sheet to display message deletion choice 2026-03-16 16:57:08 +01:00
Benoit Martins
c01f79dd20 Add settings to show past meetings and limit max width in settings views 2026-03-16 15:26:56 +01:00
Benoit Martins
2f56839937 Update Settings UI 2026-03-16 11:38:06 +01:00
Benoit Martins
0fe2b4370f Ignore authentication requests while offline 2026-03-11 15:55:50 +01:00
QuentinA
fa9be23c2d Add a wait time of up to 3 second inside the app extension, then iterate to try and receive the NOTIFY required for imdn sending 2026-03-10 13:45:43 +00:00
Benoit Martins
b8efad4980 Add Siri intent example phrases for all localizations 2026-03-10 10:46:37 +01:00
Benoit Martins
d36a0a7081 Updated CHANGELOG & bumped version code to 6.1.0 (8) 2026-03-09 16:50:53 +01:00
Benoit Martins
9555245ede Fix getFriendWithAddress to ignore nil/empty phone numbers 2026-03-09 16:45:36 +01:00
Benoit Martins
231035f109 Hide enable video setting 2026-03-09 15:28:57 +01:00
Benoit Martins
1bb372741e Add "https://" to the config URI if it doesn't exist 2026-03-09 15:01:51 +01:00
Benoit Martins
9d78fc2e29 Add AppIntentVocabulary.plist for INStartCallIntent example phrases 2026-03-06 16:07:27 +01:00
Benoit Martins
d0418ae07c Updated CHANGELOG & bumped version code to 6.1.0 (7) 2026-03-06 11:20:12 +01:00
Benoit Martins
d00257d93c Fix own reaction refresh issue 2026-03-06 10:20:06 +01:00
Benoit Martins
58d269a65f Fix reaction refresh issue 2026-03-05 17:31:38 +01:00
Benoit Martins
95a7f0987a Remove debug print statements 2026-03-05 16:34:46 +01:00
Benoit Martins
e1d3df577f Create local image library in CoreContext if it doesn’t exist 2026-03-05 16:26:42 +01:00
Benoit Martins
3b10fbb7ce Remove UIDevice.current.orientation in PopupLoadingView 2026-03-05 14:54:29 +01:00
Benoit Martins
fd2730661d Fix PopupLoadingView size 2026-03-05 14:52:31 +01:00
Benoit Martins
66f2ac6fdc Add showChatMessageContentInNotification to corePreference to control chat message content visibility in iOS notifications 2026-03-05 14:35:15 +01:00
Benoit Martins
6c5bf43062 Add contacts and suggestions to history and conversation views 2026-03-03 16:59:01 +01:00
Benoit Martins
9364e7f196 Remove AVAudioSession configuration from CallView 2026-02-27 11:04:08 +01:00
Benoit Martins
cdde88e32a Use singleSignOnClientId for SSO 2026-02-26 17:45:28 +01:00
Benoit Martins
e066ff4ee7 Update DialerBottomSheet UI and add DTMF playback 2026-02-26 16:34:57 +01:00
Benoit Martins
bfb4ac3c22 Add media and document lists to the contact detail view 2026-02-26 14:18:01 +01:00
Benoit Martins
75e96ed8a5 Automatically play next voice recording when the previous one ends 2026-02-25 16:28:40 +01:00
Benoit Martins
9cc8923e3f Refactor audio session management and add incoming message notification sound 2026-02-25 15:05:47 +01:00
Benoit Martins
db9c9f1834 Display peer address with a hidden button in conversation 2026-02-24 17:52:18 +01:00
Benoit Martins
ef09f6c412 Remove unnecessary menu 2026-02-24 16:22:03 +01:00
Benoit Martins
ca9f5ffe1f Add non-encrypted header to conversation 2026-02-24 15:58:21 +01:00
Benoit Martins
5bbbbe5d75 Add outbound proxy popup 2026-02-24 15:01:45 +01:00
Benoit Martins
f08bb865aa Add SIP proxy URL and outbound proxy to third party registration and account settings 2026-02-24 12:08:12 +01:00
Benoit Martins
0f6d8b1296 Add pending notifications banner 2026-02-24 10:36:51 +01:00
Benoit Martins
d39e4a0e34 Disable chatroom when it is not secure 2026-02-23 15:49:02 +01:00
Benoit Martins
23f5211131 Update call transfer logic to new implementation 2026-02-23 14:11:39 +01:00
Benoit Martins
1fff8d6d36 Update SPM SDK branch to Beta 2026-02-19 14:09:48 +01:00
Benoit Martins
d9d2d24d85 Refresh devices and trust when displayed contact changes 2026-02-19 14:05:02 +01:00
Benoit Martins
09863890ea Add trusted and untrusted avatar circle 2026-02-19 11:38:59 +01:00
Benoit Martins
d5d1600b4e Add trusted devices list 2026-02-16 16:14:30 +01:00
Benoit Martins
cf97da11b1 Add pagination to media and document lists 2026-02-12 12:00:52 +01:00
Benoit Martins
304651d776 Use removeAccountWithData instead of removeAccount 2026-02-12 10:54:07 +01:00
Benoit Martins
b753b5925e Fix media list UI 2026-02-12 09:44:55 +00:00
Benoit Martins
112d7bbaa9 Fix media path for message bubble 2026-02-12 09:44:55 +00:00
Benoit Martins
8223d20fc6 Add media and documents list to the conversation 2026-02-12 09:44:55 +00:00
Benoit Martins
9ac0445347 Comment out migration code in CoreContext.swift 2026-02-05 09:11:33 +00:00
Benoit Martins
2287e9a45a Update CHANGELOG.md 2026-02-03 14:58:25 +00:00
Benoit Martins
3eb09bf8b0 Add disableCallRecordings to CorePreferences 2026-02-03 15:18:44 +01:00
Benoit Martins
e3bf00dca5 Fix contacts with only a phone number 2026-02-03 13:08:58 +01:00
Benoit Martins
b3ed027c61 Normalize phone numbers in getFriendWithAddress 2026-02-02 16:41:04 +01:00
Benoit Martins
2fea9d720e Updated translations from Weblate 2026-01-29 12:25:52 +01:00
Benoit Martins
8d0f638d46 Refactor CorePreferences by adding new values 2026-01-29 10:30:08 +01:00
Benoit Martins
9072119031 Centralize App Group identifier using shared xcconfig 2026-01-27 16:59:57 +01:00
Benoit Martins
d25c19ff8e Refactor of Config and CorePreferences 2026-01-27 15:55:57 +01:00
Benoit Martins
e64c0611fb Updated CHANGELOG & bumped version code to 6.1.0 (6) 2026-01-21 13:57:41 +01:00
Benoit Martins
dc5f131cec Merge branch 'master'
#Conflicts:
#	Linphone/GeneratedGit.swift
2026-01-21 11:55:52 +01:00
Benoit Martins
02c0509c98 Add loading popup when searching messages 2026-01-20 17:23:11 +01:00
Benoit Martins
ceca9acc21 Fix first message display when displaying old messages 2026-01-20 16:36:41 +01:00
Benoit Martins
6b93a7ef5e Refactor toast system 2026-01-20 13:32:21 +01:00
Benoit Martins
ac5a23bfff Add message search feature 2026-01-20 11:29:23 +01:00
Benoit Martins
b348307b6c Update SPM dependencies 2026-01-19 11:07:34 +01:00
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
199 changed files with 16677 additions and 3957 deletions

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>IntentPhrases</key>
<array>
<dict>
<key>IntentName</key>
<string>INStartCallIntent</string>
<key>IntentExamples</key>
<array>
<string>Call John with Linphone</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -10,51 +10,136 @@ 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] - Unreleased
### Added
- Support for LDAP and CardDAV accounts
- Advanced settings for third-party SIP accounts, including outbound proxy configuration
- Display of contacts, suggestions, media, and document lists in history, conversation, and contact detail views
- Recording player with automatic playback of next recording
- Trusted/untrusted devices visualization and list management
- Pending notifications banner
- Option to show or hide chat message content in notifications
- Message editing and deletion features
### Changed
- Updated translations from Weblate
- Launch Screen (Splash Screen) refreshed
- Dialer and Popup UI improvements, including DTMF playback and layout fixes
- Audio session management refactored for stability
- Core call logs displayed when user has only one account
- Updated last message text display in conversation list
### Fixed
- Reaction refresh issues
- Media list UI and message bubble paths
- Various contact and phone number handling issues
- Chatroom disabled when insecure
- Call transfer logic updated
- Debug print statements removed
- CorePreferences and SSO improvements
- Display peer address in conversation
- Several minor crashes and UI inconsistencies resolved
## [6.0.3] 2026-01-29
### Changed
- Updated translations from Weblate
- Refreshed presence information in history details
- Refreshed displayed friend when the contacts list is updated
- Display core call logs instead of account call logs when only one account is configured
- Updated launch screen (splash screen)
- Use searchChatRoomByIdentifier instead of searchChatRoom when changing the displayed chat room
- Updated encryption handling when call state changes
- Store magicSearch.allContacts to display the full contact list on app startup
- Moved disable_chat_feature to the UI section
- Updated account parameters in CoreContext when needed
- Enabled core push notifications by default
### Fixed
- Disabled video button in audio-only mode during a call
- Disabled mediaEncryptionMandatory when media encryption is set to None
- Fixed international prefix reset in settings
- Fixed crash on defaultDomain with invalid UTF-8 strings
- Removed authentication info when logging out of an account
## [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,7 +1,7 @@
{
"images" : [
{
"filename" : "lock-simple-open-bold.svg",
"filename" : "arrow-right.svg",
"idiom" : "universal",
"scale" : "1x"
},

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="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path></svg>

After

Width:  |  Height:  |  Size: 269 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="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

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,76H100V56a28,28,0,0,1,28-28c13.51,0,25.65,9.62,28.24,22.39a12,12,0,1,0,23.52-4.78C174.87,21.5,153.1,4,128,4A52.06,52.06,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76Zm-4,128H52V100H204Z"></path></svg>

Before

Width:  |  Height:  |  Size: 370 B

View file

@ -1 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,80H96V56a32,32,0,0,1,32-32c15.37,0,29.2,11,32.16,25.59a8,8,0,0,0,15.68-3.18C171.32,24.15,151.2,8,128,8A48.05,48.05,0,0,0,80,56V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm0,128H48V96H208V208Z"></path></svg>
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_2)">
<path d="M208 88H48C43.5817 88 40 91.5817 40 96V208C40 212.418 43.5817 216 48 216H208C212.418 216 216 212.418 216 208V96C216 91.5817 212.418 88 208 88Z" stroke="black" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M88 88V56C88 45.3913 83.7857 35.2172 76.2843 27.7157C68.7828 20.2143 58.6087 16 48 16C28.65 16 11.71 29.74 8 48" stroke="black" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1_2">
<rect width="256" height="256" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 682 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,7 +1,7 @@
{
"images" : [
{
"filename" : "not_trusted.svg",
"filename" : "not-trusted.svg",
"idiom" : "universal",
"scale" : "1x"
},

View file

@ -0,0 +1,59 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
id="vector">
<defs>
<clipPath id="clip_path_1">
<path d="M 0 0 L 10 0 L 10 10 L 0 10 Z"/>
</clipPath>
<clipPath id="clip_path_2">
<path
d="M 0 0 L 10 0 L 10 10 L 0 10 Z"
clip-path="url(#clip_path_2_1)"/>
</clipPath>
<clipPath id="clip_path_2_1">
<path d="M 0 0 L 10 0 L 10 10 L 0 10 Z"/>
</clipPath>
<clipPath id="clip_path_3">
<path
d="M 0 0 L 10 0 L 10 10 L 0 10 Z"
clip-path="url(#clip_path_3_1)"/>
</clipPath>
<clipPath id="clip_path_3_1">
<path
d="M 0 0 L 10 0 L 10 10 L 0 10 Z"
clip-path="url(#clip_path_3_2)"/>
</clipPath>
<clipPath id="clip_path_3_2">
<path d="M 0 0 L 10 0 L 10 10 L 0 10 Z"/>
</clipPath>
</defs>
<g
id="wrapper"
transform="scale(2.4 2.4)">
<path
id="path"
d="M 0.769 5 C 0.769 6.122 1.215 7.199 2.008 7.992 C 2.801 8.785 3.878 9.231 5 9.231 C 6.122 9.231 7.199 8.785 7.992 7.992 C 8.785 7.199 9.231 6.122 9.231 5 C 9.231 4.107 8.948 3.236 8.423 2.513 C 7.898 1.79 7.157 1.252 6.307 0.976 C 5.458 0.7 4.542 0.7 3.693 0.976 C 2.843 1.252 2.102 1.79 1.577 2.513 C 1.052 3.236 0.769 4.107 0.769 5 Z"
fill="#dd5f5f"
stroke-width="1"/>
<path
id="path_1"
clip-path="url(#clip_path_1)"
d="M 5 2.917 C 5.23 2.917 5.417 3.103 5.417 3.333 L 5.417 5 C 5.417 5.23 5.23 5.417 5 5.417 C 4.77 5.417 4.583 5.23 4.583 5 L 4.583 3.333 C 4.583 3.103 4.77 2.917 5 2.917 Z"
fill="#364860"
stroke-width="1"/>
<path
id="path_2"
clip-path="url(#clip_path_2)"
d="M 5 6.25 C 4.77 6.25 4.583 6.437 4.583 6.667 C 4.583 6.897 4.77 7.083 5 7.083 L 5.004 7.083 C 5.234 7.083 5.421 6.897 5.421 6.667 C 5.421 6.437 5.234 6.25 5.004 6.25 L 5 6.25 Z"
fill="#364860"
stroke-width="1"/>
<path
id="path_3"
clip-path="url(#clip_path_3)"
d="M 2.98 0.539 C 3.058 0.461 3.164 0.417 3.275 0.417 L 6.725 0.417 C 6.835 0.417 6.941 0.461 7.02 0.539 L 9.461 2.98 C 9.539 3.059 9.583 3.164 9.583 3.275 L 9.583 6.725 C 9.583 6.835 9.539 6.941 9.461 7.02 L 7.02 9.461 C 6.941 9.539 6.835 9.583 6.725 9.583 L 3.275 9.583 C 3.164 9.583 3.058 9.539 2.98 9.461 L 0.539 7.02 C 0.461 6.941 0.417 6.835 0.417 6.725 L 0.417 3.275 C 0.417 3.164 0.461 3.059 0.539 2.98 L 2.98 0.539 Z M 3.448 1.25 L 1.25 3.448 L 1.25 6.552 L 3.448 8.75 L 6.552 8.75 L 8.75 6.552 L 8.75 3.448 L 6.552 1.25 L 3.448 1.25 Z"
fill="#364860"
stroke-width="1"
fill-rule="evenodd"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,13 +0,0 @@
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="5.24993" cy="5.00006" rx="4.23077" ry="4.23077" fill="#DD5F5F"/>
<g clip-path="url(#clip0_2386_24363)">
<path d="M5.24996 2.91666C5.48008 2.91666 5.66663 3.1032 5.66663 3.33332V4.99999C5.66663 5.23011 5.48008 5.41666 5.24996 5.41666C5.01984 5.41666 4.83329 5.23011 4.83329 4.99999V3.33332C4.83329 3.1032 5.01984 2.91666 5.24996 2.91666Z" fill="#364860"/>
<path d="M5.24996 6.24999C5.01984 6.24999 4.83329 6.43654 4.83329 6.66666C4.83329 6.89677 5.01984 7.08332 5.24996 7.08332H5.25413C5.48424 7.08332 5.67079 6.89677 5.67079 6.66666C5.67079 6.43654 5.48424 6.24999 5.25413 6.24999H5.24996Z" fill="#364860"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.23033 0.538695C3.30847 0.460555 3.41445 0.416656 3.52496 0.416656H6.97496C7.08547 0.416656 7.19145 0.460555 7.26959 0.538695L9.71125 2.98036C9.78939 3.0585 9.83329 3.16448 9.83329 3.27499V6.72499C9.83329 6.8355 9.78939 6.94148 9.71125 7.01962L7.26959 9.46128C7.19145 9.53942 7.08547 9.58332 6.97496 9.58332H3.52496C3.41445 9.58332 3.30847 9.53942 3.23033 9.46128L0.788665 7.01962C0.710525 6.94148 0.666626 6.8355 0.666626 6.72499V3.27499C0.666626 3.16448 0.710525 3.0585 0.788665 2.98036L3.23033 0.538695ZM3.69755 1.24999L1.49996 3.44758V6.5524L3.69755 8.74999H6.80237L8.99996 6.5524V3.44758L6.80237 1.24999H3.69755Z" fill="#364860"/>
</g>
<defs>
<clipPath id="clip0_2386_24363">
<rect width="10" height="10" fill="white" transform="translate(0.25)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

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 != AppServices.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 == AppServices.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 == AppServices.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 == AppServices.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("")
}
}
}
@ -393,20 +418,63 @@ final class ContactsManager: ObservableObject {
guard let address = address, let clonedAddress = address.clone() else {
return nil
}
clonedAddress.clean()
let sipUri = clonedAddress.asStringUriOnly()
let core = CoreContext.shared.mCore
let account = core?.defaultAccount
let normalizedIncoming = address.username.flatMap {
account?.normalizePhoneNumber(username: $0)
}
func matches(_ friend: Friend) -> Bool {
let sipMatch = friend.addresses.contains {
$0.asStringUriOnly() == sipUri
}
let phoneMatch = friend.phoneNumbers.contains { phone in
guard
let normalizedIncoming,
!normalizedIncoming.isEmpty,
let normalized = account?.normalizePhoneNumber(username: phone),
!normalized.isEmpty
else {
return false
}
return normalized == normalizedIncoming
}
return sipMatch || phoneMatch
}
var friend: Friend?
// Friend list
if let friendList = self.friendList {
friend = friendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) })
friend = friendList.friends.first(where: matches)
}
// Linphone friend list
if friend == nil, let linphoneFriendList = self.linphoneFriendList {
friend = linphoneFriendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) })
}
if friend == nil, let tempRemoteFriendList = self.tempRemoteFriendList {
friend = tempRemoteFriendList.friends.first(where: { $0.addresses.contains(where: { $0.asStringUriOnly() == sipUri }) })
}
friend = linphoneFriendList.friends.first(where: matches)
}
// Temp remote friend list
if friend == nil, let tempRemoteFriendList = self.tempRemoteFriendList {
friend = tempRemoteFriendList.friends.first(where: matches)
}
// CardDAV lists
if friend == nil, let core {
for list in core.friendsLists where list.type == .CardDAV {
friend = list.friends.first(where: matches)
if friend != nil {
break
}
}
}
return friend
}
@ -425,6 +493,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 +511,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 +684,8 @@ final class ContactsManager: ObservableObject {
}
)
self.friendListDelegate = friendListDelegateTmp
CoreContext.shared.mCore.friendsLists.forEach { friendList in
friendList.addDelegate(delegate: friendListDelegateTmp)
}
@ -530,6 +694,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 +727,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()
@ -106,12 +108,11 @@ class CoreContext: ObservableObject {
DispatchQueue.main.async {
if isConnected {
Log.info("Network is now satisfied")
ToastViewModel.shared.toastMessage = "Success_toast_network_connected"
ToastViewModel.shared.show("Success_toast_network_connected")
} else {
Log.error("Network is now \(path.status)")
ToastViewModel.shared.toastMessage = "Unavailable_network"
ToastViewModel.shared.show("Unavailable_network")
}
ToastViewModel.shared.displayToast = true
}
self.networkStatusIsConnected = isConnected
}
@ -121,11 +122,11 @@ 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: (SharedMainViewModel.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")
if let rcDir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Config.appGroupName)?
if let rcDir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: SharedMainViewModel.appGroupName)?
.appendingPathComponent("Library/Preferences/linphone") {
let rcFileUrl = rcDir.appendingPathComponent("linphonerc")
if !FileManager.default.fileExists(atPath: rcFileUrl.path) {
@ -143,22 +144,31 @@ class CoreContext: ObservableObject {
}
}
self.mCore = try? Factory.Instance.createSharedCoreWithConfig(config: Config.get(), systemContext: Unmanaged.passUnretained(coreQueue).toOpaque(), appGroupId: Config.appGroupName, mainCore: true)
self.mCore = try? Factory.Instance.createSharedCoreWithConfig(config: AppServices.config, systemContext: Unmanaged.passUnretained(coreQueue).toOpaque(), appGroupId: SharedMainViewModel.appGroupName, mainCore: true)
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
// Migration
/*
self.mCore.config!.setBool(section: "sip", key: "auto_answer_replacing_calls", value: false)
self.mCore.config!.setBool(section: "sip", key: "deliver_imdn", value: false)
self.mCore.config!.setString(section: "misc", key: "log_collection_upload_server_url", value: "https://files.linphone.org:443/http-file-transfer-server/hft.php")
@ -168,6 +178,7 @@ class CoreContext: ObservableObject {
self.mCore.imdnToEverybodyThreshold = 1
self.imdnToEverybodyThreshold = self.mCore.imdnToEverybodyThreshold == 1
//self.copyDatabaseFileToDocumentsDirectory()
*/
let shortcutsCount = self.mCore.config!.getInt(section: "ui", key: "shortcut_count", defaultValue: 0)
if shortcutsCount > 0 {
@ -200,6 +211,42 @@ class CoreContext: ObservableObject {
self.forceRemotePushToMatchVoipPushSettings(account: acc)
}
let fm = FileManager.default
let folderURL = FileUtil.sharedContainerUrl().appendingPathComponent("Library/Images")
if !fm.fileExists(atPath: folderURL.path) {
do {
try fm.createDirectory(
at: folderURL,
withIntermediateDirectories: true,
attributes: nil
)
print("Images directory created.")
} catch {
print("Error creating directory: \(error)")
}
} else {
print("Images directory already exists.")
}
let container = FileUtil.sharedContainerUrl()
let recordingsDir = container.appendingPathComponent("Library/Recordings")
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 +255,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) }
@ -254,6 +295,11 @@ class CoreContext: ObservableObject {
}
}
}, onAuthenticationRequested: { (core: Core, authInfo: AuthInfo, method: AuthMethod) in
guard self.networkStatusIsConnected else {
Log.warn("[CoreContext] Authentication requested while device is offline, ignoring")
return
}
if method == .Bearer {
if let server = authInfo.authorizationServer, !server.isEmpty {
Log.info("Authentication requested method is Bearer, starting Single Sign On activity with server URL \(server) and username \(authInfo.username ?? "")")
@ -293,18 +339,17 @@ class CoreContext: ObservableObject {
Log.info("[CoreContext] Transferred call \(transferred.remoteAddress!.asStringUriOnly()) state changed \(callState)")
DispatchQueue.main.async {
if callState == Call.State.Connected {
ToastViewModel.shared.toastMessage = "Success_toast_call_transfer_successful"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Success_toast_call_transfer_successful")
} else if callState == Call.State.OutgoingProgress {
ToastViewModel.shared.toastMessage = "Success_toast_call_transfer_in_progress"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Success_toast_call_transfer_in_progress")
} else if callState == Call.State.End || callState == Call.State.Error {
ToastViewModel.shared.toastMessage = "Failed_toast_call_transfer_failed"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_toast_call_transfer_failed")
}
}
}, onConfiguringStatus: { (_: Core, status: ConfiguringState, message: String) in
Log.info("New configuration state is \(status) = \(message)\n")
let themeMainColor = AppServices.corePreferences.themeMainColor
SharedMainViewModel.shared.updateConfigChanges()
DispatchQueue.main.async {
if status == ConfiguringState.Successful {
var accountModels: [AccountModel] = []
@ -312,14 +357,15 @@ 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
if info.starts(with: "https") {
DispatchQueue.main.async {
UIPasteboard.general.setValue(info, forPasteboardType: UTType.plainText.identifier)
ToastViewModel.shared.toastMessage = "Success_send_logs"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Success_send_logs")
}
}
}, onAccountRegistrationStateChanged: { (core: Core, account: Account, state: RegistrationState, message: String) in
@ -369,8 +415,7 @@ class CoreContext: ObservableObject {
self.loggedIn = false
if self.networkStatusIsConnected {
// If network is disconnected, a toast message with key "Unavailable_network" should already be displayed
ToastViewModel.shared.toastMessage = "Registration_failed"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Registration_failed")
}
}
@ -406,6 +451,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 +494,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")
}
}
}
@ -479,7 +547,7 @@ class CoreContext: ObservableObject {
}
func copyDatabaseFileToDocumentsDirectory() {
if let rcDir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Config.appGroupName)?
if let rcDir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: SharedMainViewModel.appGroupName)?
.appendingPathComponent("Library/Application Support/linphone") {
let rcFileUrl = rcDir.appendingPathComponent("linphone.db")
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
@ -497,6 +565,34 @@ class CoreContext: ObservableObject {
}
}
enum AppServices {
private static var _config: Config?
static var configIfAvailable: Config? {
if let existing = _config {
return existing
}
_config = Config.newForSharedCore(
appGroupId: Bundle.main.object(forInfoDictionaryKey: "APP_GROUP_NAME") as? String
?? {
fatalError("APP_GROUP_NAME not defined in Info.plist")
}(),
configFilename: "linphonerc",
factoryConfigFilename: FileUtil.bundleFilePath("linphonerc-factory")
)
return _config
}
static var config: Config {
guard let config = configIfAvailable else {
fatalError("AppServices.config accessed before it was available")
}
return config
}
static let corePreferences = CorePreferences(config: config)
}
// swiftlint:enable line_length
// swiftlint:enable cyclomatic_complexity
// swiftlint:enable identifier_name

View file

@ -1,7 +1,7 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
* This file is part of Linphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -20,252 +20,425 @@
import Foundation
import linphonesw
class CorePreferences {
static var printLogsInLogcat: Bool {
class CorePreferences: ObservableObject {
private let config: Config
init(config: Config) {
self.config = config
}
var acceptEarlyMedia: Bool {
get {
return Config.get().getBool(section: "app", key: "debug", defaultValue: true)
config.getBool(section: "sip", key: "incoming_calls_early_media", defaultValue: false)
}
set {
Config.get().setBool(section: "app", key: "debug", value: newValue)
config.setBool(section: "sip", key: "incoming_calls_early_media", value: newValue)
}
}
var allowOutgoingEarlyMedia: Bool {
get {
config.getBool(section: "sip", key: "real_early_media", defaultValue: false)
}
set {
config.setBool(section: "sip", key: "real_early_media", value: newValue)
}
}
var automaticallyStartCallRecording: Bool {
get {
config.getBool(section: "app", key: "auto_start_call_record", defaultValue: false)
}
set {
config.setBool(section: "app", key: "auto_start_call_record", value: newValue)
}
}
var changeMainColorAllowed: Bool {
get {
config.getBool(section: "ui", key: "change_main_color_allowed", defaultValue: false)
}
set {
config.setBool(section: "ui", key: "change_main_color_allowed", value: newValue)
}
}
var checkForUpdateServerUrl: String {
get {
let raw = config.getString(section: "misc", key: "version_check_url_root", defaultString: "")
return safeString(raw, defaultValue: "")
}
set {
config.setString(section: "misc", key: "version_check_url_root", value: newValue)
}
}
var conditionsAndPrivacyPolicyAccepted: Bool {
get {
config.getBool(section: "app", key: "read_and_agree_terms_and_privacy", defaultValue: false)
}
set {
config.setBool(section: "app", key: "read_and_agree_terms_and_privacy", value: newValue)
}
}
var contactsFilter: String {
get {
let raw = config.getString(section: "ui", key: "contacts_filter", defaultString: "")
return safeString(raw, defaultValue: "")
}
set {
config.setString(section: "ui", key: "contacts_filter", value: newValue)
}
}
var darkMode: Int {
get {
if !darkModeAllowed { return 0 }
return config.getInt(section: "app", key: "dark_mode", defaultValue: -1)
}
set {
config.setInt(section: "app", key: "dark_mode", value: newValue)
}
}
var darkModeAllowed: Bool {
get {
config.getBool(section: "ui", key: "dark_mode_allowed", defaultValue: true)
}
set {
config.setBool(section: "ui", key: "dark_mode_allowed", value: newValue)
}
}
var defaultDomain: String {
get {
let raw = config.getString(section: "app", key: "default_domain", defaultString: "sip.linphone.org")
return safeString(raw, defaultValue: "sip.linphone.org")
}
set {
config.setString(section: "app", key: "default_domain", value: newValue)
}
}
var defaultPass: String {
get {
config.getString(section: "app", key: "pass", defaultString: "")
}
set {
config.setString(section: "app", key: "pass", value: newValue)
}
}
var defaultUsername: String {
get {
config.getString(section: "app", key: "user", defaultString: "")
}
set {
config.setString(section: "app", key: "user", value: newValue)
}
}
var deviceName: String {
get {
let raw = config.getString(section: "app", key: "device", defaultString: "").trimmingCharacters(in: .whitespaces)
return safeString(raw, defaultValue: "")
}
set {
config.setString(section: "app", key: "device", value: newValue.trimmingCharacters(in: .whitespaces))
}
}
var hideContactEdition: Bool {
get {
config.getBool(section: "ui", key: "hide_contact_edition", defaultValue: false)
}
set {
config.setBool(section: "ui", key: "hide_contact_edition", value: newValue)
}
}
static var firstLaunch: Bool {
var disableAddContact: Bool {
get {
return Config.get().getBool(section: "app", key: "first_6.0_launch", defaultValue: true)
config.getBool(section: "ui", key: "disable_add_contact", defaultValue: false)
}
set {
Config.get().setBool(section: "app", key: "first_6.0_launch", value: newValue)
config.setBool(section: "ui", key: "disable_add_contact", value: newValue)
}
}
static var linphoneConfigurationVersion: Int {
var disableCallRecordings: Bool {
get {
return Config.get().getInt(section: "app", key: "config_version", defaultValue: 52005)
config.getBool(section: "ui", key: "disable_call_recordings_feature", defaultValue: false)
}
set {
Config.get().setInt(section: "app", key: "config_version", value: newValue)
config.setBool(section: "ui", key: "disable_call_recordings_feature", value: newValue)
}
}
static var checkForUpdateServerUrl: String {
var disableChatFeature: Bool {
get {
return Config.get().getString(section: "misc", key: "version_check_url_root", defaultString: "")
config.getBool(section: "ui", key: "disable_chat_feature", defaultValue: false)
}
set {
Config.get().setString(section: "misc", key: "version_check_url_root", value: newValue)
config.setBool(section: "ui", key: "disable_chat_feature", value: newValue)
}
}
static var conditionsAndPrivacyPolicyAccepted: Bool {
var disableMeetings: Bool {
get {
return Config.get().getBool(section: "app", key: "read_and_agree_terms_and_privacy", defaultValue: false)
config.getBool(section: "ui", key: "disable_meetings_feature", defaultValue: false)
}
set {
Config.get().setBool(section: "app", key: "read_and_agree_terms_and_privacy", value: newValue)
config.setBool(section: "ui", key: "disable_meetings_feature", value: newValue)
}
}
static var publishPresence: Bool {
var earlymediaContentExtCatIdentifier: String {
get {
return Config.get().getBool(section: "app", key: "publish_presence", defaultValue: true)
config.getString(section: "app", key: "extension_category", defaultString: "")
}
set {
Config.get().setBool(section: "app", key: "publish_presence", value: newValue)
config.setString(section: "app", key: "extension_category", value: newValue)
}
}
static var keepServiceAlive: Bool {
var enableSecureMode: Bool {
get {
return Config.get().getBool(section: "app", key: "keep_service_alive", defaultValue: false)
config.getBool(section: "ui", key: "enable_secure_mode", defaultValue: true)
}
set {
Config.get().setBool(section: "app", key: "keep_service_alive", value: newValue)
config.setBool(section: "ui", key: "enable_secure_mode", value: newValue)
}
}
static var deviceName: String {
var firstLaunch: Bool {
get {
return Config.get().getString(section: "app", key: "device", defaultString: "").trimmingCharacters(in: .whitespaces)
config.getBool(section: "app", key: "first_6.0_launch", defaultValue: true)
}
set {
Config.get().setString(section: "app", key: "device", value: newValue.trimmingCharacters(in: .whitespaces))
config.setBool(section: "app", key: "first_6.0_launch", value: newValue)
}
}
static var routeAudioToSpeakerWhenVideoIsEnabled: Bool {
var friendListInWhichStoreNewlyCreatedFriends: String {
get {
return Config.get().getBool(section: "app", key: "route_audio_to_speaker_when_video_enabled", defaultValue: true)
config.getString(section: "app", key: "friend_list_to_store_newly_created_contacts", defaultString: "Linphone address-book")
}
set {
Config.get().setBool(section: "app", key: "route_audio_to_speaker_when_video_enabled", value: newValue)
config.setString(section: "app", key: "friend_list_to_store_newly_created_contacts", value: newValue)
}
}
static var automaticallyStartCallRecording: Bool {
var hideSettings: Bool {
get {
return Config.get().getBool(section: "app", key: "auto_start_call_record", defaultValue: false)
config.getBool(section: "ui", key: "hide_settings", defaultValue: false)
}
set {
Config.get().setBool(section: "app", key: "auto_start_call_record", value: newValue)
config.setBool(section: "ui", key: "hide_settings", value: newValue)
}
}
static var showDialogWhenCallingDeviceUuidDirectly: Bool {
var hideSipAddresses: Bool {
get {
return Config.get().getBool(section: "app", key: "show_confirmation_dialog_zrtp_trust_call", defaultValue: true)
config.getBool(section: "ui", key: "hide_sip_addresses", defaultValue: false)
}
set {
Config.get().setBool(section: "app", key: "show_confirmation_dialog_zrtp_trust_call", value: newValue)
config.setBool(section: "ui", key: "hide_sip_addresses", value: newValue)
}
}
static var markConversationAsReadWhenDismissingMessageNotification: Bool {
var keepServiceAlive: Bool {
get {
return Config.get().getBool(section: "app", key: "mark_as_read_notif_dismissal", defaultValue: false)
config.getBool(section: "app", key: "keep_service_alive", defaultValue: false)
}
set {
Config.get().setBool(section: "app", key: "mark_as_read_notif_dismissal", value: newValue)
config.setBool(section: "app", key: "keep_service_alive", value: newValue)
}
}
static var contactsFilter: String {
var linphoneConfigurationVersion: Int {
get {
return Config.get().getString(section: "ui", key: "contacts_filter", defaultString: "")
config.getInt(section: "app", key: "config_version", defaultValue: 52005)
}
set {
Config.get().setString(section: "ui", key: "contacts_filter", value: newValue)
config.setInt(section: "app", key: "config_version", value: newValue)
}
}
static var showFavoriteContacts: Bool {
var markConversationAsReadWhenDismissingMessageNotification: Bool {
get {
return Config.get().getBool(section: "ui", key: "show_favorites_contacts", defaultValue: true)
config.getBool(section: "app", key: "mark_as_read_notif_dismissal", defaultValue: false)
}
set {
Config.get().setBool(section: "ui", key: "show_favorites_contacts", value: newValue)
config.setBool(section: "app", key: "mark_as_read_notif_dismissal", value: newValue)
}
}
static var voiceRecordingMaxDuration: Int {
var maxAccountsCount: Int {
get {
return Config.get().getInt(section: "app", key: "voice_recording_max_duration", defaultValue: 600000)
config.getInt(section: "ui", key: "max_account", defaultValue: 0)
}
set {
Config.get().setInt(section: "app", key: "voice_recording_max_duration", value: newValue)
config.setInt(section: "ui", key: "max_account", value: newValue)
}
}
static var darkMode: Int {
var onlyAllowEarpieceDuringCall: Bool {
get {
if !darkModeAllowed { return 0 }
return Config.get().getInt(section: "app", key: "dark_mode", defaultValue: -1)
config.getBool(section: "ui", key: "only_allow_earpiece_during_call", defaultValue: false)
}
set {
Config.get().setInt(section: "app", key: "dark_mode", value: newValue)
config.setBool(section: "ui", key: "only_allow_earpiece_during_call", value: newValue)
}
}
static var enableSecureMode: Bool {
var printLogsInLogcat: Bool {
get {
return Config.get().getBool(section: "ui", key: "enable_secure_mode", defaultValue: true)
config.getBool(section: "app", key: "debug", defaultValue: true)
}
set {
Config.get().setBool(section: "ui", key: "enable_secure_mode", value: newValue)
config.setBool(section: "app", key: "debug", value: newValue)
}
}
static var themeMainColor: String {
var publishPresence: Bool {
get {
return Config.get().getString(section: "ui", key: "theme_main_color", defaultString: "orange")
config.getBool(section: "app", key: "publish_presence", defaultValue: true)
}
set {
Config.get().setString(section: "ui", key: "theme_main_color", value: newValue)
config.setBool(section: "app", key: "publish_presence", value: newValue)
}
}
static var darkModeAllowed: Bool {
return Config.get().getBool(section: "ui", key: "dark_mode_allowed", defaultValue: true)
}
static var changeMainColorAllowed: Bool {
return Config.get().getBool(section: "ui", key: "change_main_color_allowed", defaultValue: false)
}
static var hideSettings: Bool {
return Config.get().getBool(section: "ui", key: "hide_settings", defaultValue: false)
}
static var maxAccountsCount: Int {
return Config.get().getInt(section: "ui", key: "max_account", defaultValue: 0)
}
/*
static var configPath: String {
return context.view.window?.rootViewController?.view.frame.origin.x ?? "" + "/.linphonerc"
}
static var factoryConfigPath: String {
return context.view.window?.rootViewController?.view.frame.origin.x ?? "" + "/linphonerc"
}
func copyAssetsFromPackage() {
copy(from: "linphonerc_default", to: configPath)
copy(from: "linphonerc_factory", to: factoryConfigPath, overrideIfExists: true)
}
*/
static var vfsEnabled: Bool {
var pushNotificationsInterval: Int {
get {
return Config.get().getBool(section: "app", key: "vfs_enabled", defaultValue: false)
config.getInt(section: "net", key: "pn-call-remote-push-interval", defaultValue: 3)
}
set {
Config.get().setBool(section: "app", key: "vfs_enabled", value: newValue)
config.setInt(section: "net", key: "pn-call-remote-push-interval", value: newValue)
}
}
static var acceptEarlyMedia: Bool {
var routeAudioToSpeakerWhenVideoIsEnabled: Bool {
get {
return Config.get().getBool(section: "sip", key: "incoming_calls_early_media", defaultValue: false)
config.getBool(section: "app", key: "route_audio_to_speaker_when_video_enabled", defaultValue: true)
}
set {
Config.get().setBool(section: "sip", key: "incoming_calls_early_media", value: newValue)
config.setBool(section: "app", key: "route_audio_to_speaker_when_video_enabled", value: newValue)
}
}
static var allowOutgoingEarlyMedia: Bool {
var serveraddress: String {
get {
return Config.get().getBool(section: "sip", key: "real_early_media", defaultValue: false)
config.getString(section: "app", key: "server", defaultString: "")
}
set {
Config.get().setBool(section: "sip", key: "real_early_media", value: newValue)
config.setString(section: "app", key: "server", value: newValue)
}
}
static var defaultDomain: String {
var showChatMessageContentInNotification: Bool {
get {
return Config.get().getString(section: "app", key: "default_domain", defaultString: "sip.linphone.org")
config.getBool(section: "ui", key: "display_notification_content", defaultValue: true)
}
set {
Config.get().setString(section: "app", key: "default_domain", value: newValue)
config.setBool(section: "ui", key: "display_notification_content", 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 disableMeetings: Bool {
var showDeveloperSettings: Bool {
get {
return Config.get().getBool(section: "ui", key: "disable_meetings_feature", defaultValue: false)
config.getBool(section: "ui", key: "show_developer_settings", defaultValue: false)
}
set {
Config.get().setBool(section: "ui", key: "disable_meetings_feature", value: newValue)
config.setBool(section: "ui", key: "show_developer_settings", value: newValue)
}
}
var showDialogWhenCallingDeviceUuidDirectly: Bool {
get {
config.getBool(section: "app", key: "show_confirmation_dialog_zrtp_trust_call", defaultValue: true)
}
set {
config.setBool(section: "app", key: "show_confirmation_dialog_zrtp_trust_call", value: newValue)
}
}
var showFavoriteContacts: Bool {
get {
config.getBool(section: "ui", key: "show_favorites_contacts", defaultValue: true)
}
set {
config.setBool(section: "ui", key: "show_favorites_contacts", value: newValue)
}
}
var showPastMeetings: Bool {
get {
config.getBool(section: "ui", key: "show_past_meetings", defaultValue: true)
}
set {
DispatchQueue.main.async {
self.objectWillChange.send()
}
config.setBool(section: "ui", key: "show_past_meetings", value: newValue)
}
}
var singleSignOnClientId: String {
get {
config.getString(section: "app", key: "oidc_client_id", defaultString: "linphone")
}
set {
config.setString(section: "app", key: "oidc_client_id", value: newValue)
}
}
var teamID: String {
get {
config.getString(section: "app", key: "team_id", defaultString: "")
}
set {
config.setString(section: "app", key: "team_id", value: newValue)
}
}
var themeAboutPictureUrl: String? {
get {
config.getString(section: "ui", key: "theme_about_picture_url", defaultString: nil)
}
set {
config.setString(section: "ui", key: "theme_about_picture_url", value: newValue)
}
}
var themeMainColor: String {
get {
let raw = config.getString(section: "ui", key: "theme_main_color", defaultString: "orange")
return safeString(raw, defaultValue: "orange")
}
set {
config.setString(section: "ui", key: "theme_main_color", value: newValue)
}
}
var vfsEnabled: Bool {
get {
config.getBool(section: "app", key: "vfs_enabled", defaultValue: false)
}
set {
config.setBool(section: "app", key: "vfs_enabled", value: newValue)
}
}
var voiceRecordingMaxDuration: Int {
get {
config.getInt(section: "app", key: "voice_recording_max_duration", defaultValue: 600000)
}
set {
config.setInt(section: "app", key: "voice_recording_max_duration", value: newValue)
}
}
@ -283,4 +456,12 @@ class CorePreferences {
}
}
}
private 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 = "127e12b38"
public static let tag = "6.1.0-alpha"
}

View file

@ -2,8 +2,20 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>TFInternalTestingOnly</key>
<false/>
<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 +127,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 +145,8 @@
<string>audio</string>
</array>
<key>UILaunchScreen</key>
<dict>
<key>UIImageName</key>
<string>linphone</string>
</dict>
<false/>
<key>APP_GROUP_NAME</key>
<string>$(APP_GROUP_NAME)</string>
</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,8 @@
import SwiftUI
import linphonesw
import UserNotifications
import Intents
import PushKit
let accountTokenNotification = Notification.Name("AccountCreationTokenReceived")
var displayedChatroomPeerAddr: String?
@ -108,7 +110,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) {
@ -129,28 +152,69 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
@main
struct LinphoneApp: App {
@Environment(\.scenePhase) var scenePhase
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@State private var configAvailable = AppServices.configIfAvailable != nil
private let earlyPushDelegate = EarlyPushkitDelegate()
private let voipRegistry = PKPushRegistry(queue: coreQueue)
init() {
if !configAvailable {
voipRegistry.delegate = earlyPushDelegate
voipRegistry.desiredPushTypes = [.voIP]
waitForConfig()
} else {
let _ = CoreContext.shared
}
}
var body: some Scene {
WindowGroup {
if configAvailable {
AppView(delegate: delegate)
} else {
SplashScreen(showSpinner: true)
.onAppear {
waitForConfig()
}
}
}
}
private func waitForConfig() {
if AppServices.configIfAvailable != nil {
let _ = CoreContext.shared
configAvailable = true
} else {
Log.warn("AppServices.config not available yet, retrying in 1s...")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
waitForConfig()
}
}
}
}
struct AppView: View {
@Environment(\.scenePhase) var scenePhase
let delegate: AppDelegate
@StateObject private var coreContext = CoreContext.shared
@StateObject private var navigationManager = NavigationManager()
@StateObject private var telecomManager = TelecomManager.shared
@StateObject private var sharedMainViewModel = SharedMainViewModel.shared
var body: some Scene {
WindowGroup {
RootView(
coreContext: coreContext,
telecomManager: telecomManager,
sharedMainViewModel: sharedMainViewModel,
navigationManager: navigationManager,
appDelegate: delegate
)
.environmentObject(coreContext)
.environmentObject(navigationManager)
.environmentObject(telecomManager)
.environmentObject(sharedMainViewModel)
}
var body: some View {
RootView(
coreContext: coreContext,
telecomManager: telecomManager,
sharedMainViewModel: sharedMainViewModel,
navigationManager: navigationManager,
appDelegate: delegate
)
.environmentObject(coreContext)
.environmentObject(navigationManager)
.environmentObject(telecomManager)
.environmentObject(sharedMainViewModel)
.onChange(of: scenePhase) { newPhase in
if !telecomManager.callInProgress {
switch newPhase {
@ -240,6 +304,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 +339,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 +358,6 @@ struct MainViewSwitcher: View {
navigationManager.openChatRoom(callId: callId, peerAddr: peerAddr, localAddr: localAddr)
}
}
.id(colors.theme.name)
}
}

View file

@ -0,0 +1,3 @@
"notification_earpiece_enforcement_message" = "Si us plau, utilitzeu només l'auricular. Les altres sortides d'àudio estan desactivades.";
"welcome_page_2_title" = "Assegurada";
"welcome_page_title" = "Benvingut";

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";
@ -254,6 +254,7 @@
"new_conversation_title" = "Nová konverzace";
"next" = "Další";
"notification_missed_call_title" = "Zmeškaný hovor";
"notification_earpiece_enforcement_message" = "Používejte pouze sluchátko. Ostatní zvukové výstupy jsou zakázány.";
"operation_in_progress_overlay" = "Operace probíhá, prosím počkejte";
"or" = "nebo";
"settings_advanced_allow_outgoing_early_media_title" = "Přenášet zvuk při odchozím hovoru (early media)";
@ -474,9 +475,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 +523,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" = "Early Media erlauben";
"settings_advanced_allow_outgoing_early_media_title" = "Ausgehendes Early Media erlauben";
"settings_advanced_audio_codecs_title" = "Audio-Codecs";
"settings_advanced_audio_devices_title" = "Audiogeräte";
"settings_advanced_device_id" = "Gerät ID";
@ -61,16 +61,16 @@
"assistant_third_party_sip_account_warning_ok" = "Ich verstehe";
"bottom_navigation_calls_label" = "Anrufe";
"bottom_navigation_contacts_label" = "Kontakte";
"bottom_navigation_conversations_label" = "Gespräche";
"bottom_navigation_conversations_label" = "Chats";
"bottom_navigation_meetings_label" = "Besprechungen";
"call_action_blind_transfer" = "Weiterleiten";
"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";
@ -123,7 +123,7 @@
"contact_details_remove_from_favourites" = "Aus Favoriten entfernen";
"contact_details_share" = "Teilen";
"contact_dialog_delete_message" = "Dieser Kontakt wird endgültig entfernt.";
"contact_dialog_pick_phone_number_or_sip_address_title" = "Eine Nummer oder SIP Adresse wählen";
"contact_dialog_pick_phone_number_or_sip_address_title" = "Eine Nummer oder SIP-Adresse wählen";
"contact_edit_title" = "Kontakt bearbeiten";
"contact_editor_company" = "Unternehmen";
"contact_editor_dialog_abort_confirmation_message" = "Alle Änderungen werden verloren";
@ -136,17 +136,17 @@
"contacts_list_all_contacts_title" = "Alle Kontakte";
"contacts_list_empty" = "Im Moment kein Kontakt…";
"contacts_list_favourites_title" = "Favoriten";
"contacts_list_filter_popup_see_all" = "Alles sehen";
"contacts_list_filter_popup_see_all" = "Alle Kontakte";
"conversation_action_call" = "Anruf";
"conversation_action_configure_ephemeral_messages" = "kurzlebiger Nachrichten konfigurieren";
"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";
"conversation_dialog_set_subject" = "Gesprächsbetreff festlegen";
"conversation_dialog_set_subject" = "Chat-Betreff festlegen";
"conversation_dialog_subject_hint" = "Gesprächsbetreff";
"conversation_ephemeral_messages_duration_disabled" = "Deaktiviert";
"conversation_ephemeral_messages_duration_one_day" = "1 Tag";
@ -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";
@ -167,19 +167,19 @@
"conversation_info_admin_menu_set_participant_admin" = "Administratorrechte erteilen";
"conversation_info_admin_menu_unset_participant_admin" = "Administratorrechte entfernen";
"conversation_info_confirm_start_group_call_dialog_message" = "Alle Teilnehmer werden angerufen.";
"conversation_info_confirm_start_group_call_dialog_title" = "Einen Gruppengespräch starten?";
"conversation_info_confirm_start_group_call_dialog_title" = "Gruppenanruf starten?";
"conversation_info_delete_history_action" = "Verlauf löschen";
"conversation_info_menu_add_to_contacts" = "Zu Kontakte hinzufügen";
"conversation_info_menu_go_to_contact" = "Remove admin rights";
"conversation_info_participant_is_admin_label" = "Administrator";
"conversation_invalid_participant_due_to_security_mode_toast" = "Aufgrund von Sicherheitsbeschränkungen kann keine Konversation mit einem Teilnehmer erstellt werden, der sich nicht in derselben Domäne befindet!";
"conversation_menu_configure_ephemeral_messages" = "Kurzlebiger Nachrichten";
"conversation_menu_go_to_info" = "Gespräch Info";
"conversation_menu_go_to_info" = "Chat-Info";
"conversation_message_forward_cancelled_toast" = "Die Nachrichtenweiterleitung wurde abgebrochen";
"conversation_message_forwarded_toast" = "Nachricht wurde weitergeleitet";
"conversation_message_meeting_updated_label" = "Besprechung wurde aktualisiert";
"conversation_text_field_hint" = "Sag etwas…";
"conversations_list_empty" = "Kein Gespräch im Moment…";
"conversations_list_empty" = "Kein Chat im Moment…";
"conversation_take_picture_label" = "Foto aufnehmen";
"conversation_pick_file_from_gallery_label" = "Galerie öffnen";
"conversation_pick_any_file_label" = "Datei auswählen";
@ -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,19 +288,20 @@
"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";
"notification_earpiece_enforcement_message" = "Bitte verwenden Sie nur den Hörer. Andere Audioausgaben sind deaktiviert.";
"operation_in_progress_overlay" = "Vorgang wird ausgeführt, bitte warten";
"or" = "oder";
"password" = "Passwort";
@ -324,24 +325,24 @@
"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_title" = "Gespräche";
"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" = "Chats";
"settings_meetings_default_layout_title" = "Standardlayout";
"settings_meetings_layout_active_speaker_label" = "Aktiver Lautsprecher";
"settings_meetings_layout_mosaic_label" = "Mosaik";
@ -358,8 +359,162 @@
"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" = "Dank unserer **Ende-zu-Ende-Verschlüsselung** ist Ihre Verbindung 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";
"authentication_id" = "Authentifizierungs ID (falls anders)";
"[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_web_platform_link" = "";
"conversation_end_to_end_encrypted_bottom_sheet_link" = "https://linphone.org/en/features/#security";
"sip.linphone.org" = "sip.linphone.org";
"website_contact_url" = "";
"web_platform_forgotten_password_url" = "";

View file

@ -47,11 +47,14 @@
"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!";
"account_settings_push_notification_title" = "Allow push notifications";
"account_settings_sip_proxy_url_title" = "SIP proxy server URL";
"account_settings_sip_proxy_url_title" = "Registrar URI";
"account_settings_outbound_proxy_title" = "Outbound SIP Proxy URI";
"account_settings_stun_server_url_title" = "STUN/TURN server URL";
"account_settings_title" = "Account settings";
"account_settings_turn_password_title" = "TURN password";
@ -93,6 +96,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";
@ -127,7 +131,6 @@
"call_history_deleted_toast" = "History has been deleted";
"call_not_encrypted" = "Call is not encrypted";
"call_outgoing" = "Outgoing call";
"call_srtp_point_to_point_encrypted" = "Point-to-point encrypted by SRTP";
"call_state_connected" = "Active";
"call_state_paused" = "Paused";
"call_state_paused_by_remote" = "Paused by remote";
@ -135,12 +138,21 @@
"call_stats_audio_title" = "Audio";
"call_stats_media_encryption_title" = "Media encryption";
"call_stats_video_title" = "Video";
"call_transfer_current_call_title" = "Transfer call";
"call_transfer_current_call_title" = "Transfer %@ to…";
"call_transfer_active_calls_label" = "Current calls";
"call_transfer_no_active_call_label" = "No other call";
"call_transfer_confirm_dialog_tittle" = "Confirm call transfer";
"call_transfer_confirm_dialog_message" = "You're about to transfer call %1$@ to %2$@.";
"call_transfer_failed_toast" = "Call transfer failed!";
"call_transfer_in_progress_toast" = "Call is being transferred";
"call_transfer_successful_toast" = "Call has been successfully transferred";
"call_waiting_for_encryption_info" = "Waiting for encryption…";
"call_conference_end_to_end_encrypted" = "End-to-end encrypted";
"call_zrtp_point_to_point_encrypted" = "Point-to-point encrypted by ZRTP";
"call_srtp_point_to_point_encrypted" = "Point-to-point encrypted by SRTP";
"call_zrtp_end_to_end_encrypted" = "End-to-end encrypted by ZRTP";
"call_zrtp_sas_validation_required" = "Validation required";
"call_zrtp_sas_validation_skip" = "Skip";
"calls_count_label" = "%@ calls";
@ -169,8 +181,15 @@
"contact_details_numbers_and_addresses_title" = "Phone numbers &amp; SIP addresses";
"contact_details_remove_from_favourites" = "Remove from favourites";
"contact_details_share" = "Share";
"contact_dialog_delete_message" = "This contact will be definitively removed.";
"contact_details_trust_title" = "Trust";
"contact_details_no_device_found" = "No device found…";
"contact_details_trusted_devices_count" = "Number of trusted devices:";
"contact_dialog_increase_trust_level_title" = "Increase trust level";
"contact_dialog_increase_trust_level_message" = "You're about to make a call to %1$@'s device %2$@.\nDo you want to make the call?";
"contact_dialog_devices_trust_help_title" = "Trust level";
"contact_dialog_devices_trust_help_message" = "Check all of your contact devices to make sure your communications will be secured an unaltered.\nWhen all will be verified, you'll reach maximum trust level.";
"contact_dialog_delete_title" = "Delete %@?";
"contact_dialog_delete_message" = "This contact will be definitively removed.";
"contact_dialog_pick_phone_number_or_sip_address_title" = "Choose a number or a SIP address";
"contact_edit_title" = "Edit contact";
"contact_editor_company" = "Company";
@ -179,6 +198,8 @@
"contact_editor_first_name" = "First name";
"contact_editor_job_title" = "Job title";
"contact_editor_last_name" = "Last name";
"contact_make_call_check_device_trust" = "Verify";
"contact_device_without_name" = "Unnamed device";
"contact_message_action" = "Message";
"contact_new_title" = "New contact";
"contact_video_call_action" = "Video call";
@ -187,6 +208,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 +222,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" = "Delete for me";
"conversation_dialog_delete_for_everyone_label" = "Delete 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.";
@ -240,20 +269,34 @@
"conversation_info_participant_is_admin_label" = "Admin";
"conversation_info_participants_list_title" = "Group members (%d)";
"conversation_invalid_participant_due_to_security_mode_toast" = "Can't create conversation with a participant not on the same domain due to security restrictions!";
"conversation_menu_configure_ephemeral_messages" = "Ephemeral messages";
"conversation_menu_search_in_messages" = "Search";
"conversation_menu_go_to_info" = "Conversation info";
"conversation_menu_configure_ephemeral_messages" = "Ephemeral messages";
"conversation_menu_media_files" = "Media";
"conversation_menu_documents_files" = "Documents";
"conversation_no_media_found" = "No media found…";
"conversation_no_document_found" = "No document found…";
"conversation_media_list_title" = "Shared media";
"conversation_document_list_title" = "Shared documents";
"conversation_details_media_documents_title" = "Media & documents";
"conversation_message_forward_cancelled_toast" = "Message forward was cancelled";
"conversation_message_forwarded_toast" = "Message was forwarded";
"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_search_no_match_found" = "No matching result found";
"conversation_search_results_limit_reached_label" = "Search results limit reached, refine your search";
"conversation_text_field_hint" = "Say something…";
"conversations_list_empty" = "No conversation for the moment…";
"conversation_take_picture_label" = "Take picture";
"conversation_pick_file_from_gallery_label" = "Open gallery";
"conversation_pick_any_file_label" = "Pick file";
"conversation_file_cant_be_opened_error_toast" = "File can't be opened!";
"conversation_warning_disabled_because_not_secured_title" = "This conversation is not encrypted!";
"conversation_warning_disabled_because_not_secured_subtitle" = "Messages aren't end-to-end encrypted, make sure you don't share sensitive information!";
"debug_logs_copied_to_clipboard_toast" = "Debug logs copied to clipboard";
"Default" = "Default";
"default_account_disabled" = "Selected account is currently disabled";
@ -265,8 +308,10 @@
"dialog_continue" = "Continue";
"dialog_deny" = "Deny";
"dialog_install" = "Install";
"dialog_do_not_show_anymore" = "Do not show this dialog anymore";
"dialog_no" = "No";
"dialog_ok" = "OK";
"dialog_confirm" = "Confirm";
"dialog_understood" = "Understood";
"dialog_yes" = "Yes";
"drawer_menu_account_connection_status_cleared" = "Disabled";
"drawer_menu_account_connection_status_connected" = "Connected";
@ -325,6 +370,7 @@
"Interoperable mode" = "Interoperable mode";
"list_filter_no_result_found" = "No result found…";
"manage_account_add_picture" = "Add a picture";
"manage_account_delete" = "Sign out";
"manage_account_details_title" = "Details";
"manage_account_device_last_connection" = "Last connection:";
@ -333,6 +379,8 @@
"manage_account_dialog_international_prefix_help_message" = "Pick your country to allow Linphone to match your contacts.";
"manage_account_dialog_remove_account_message" = "If you wish to delete your account permanently, go to: https://sip.linphone.org";
"manage_account_dialog_remove_account_title" = "Sign out of your account?";
"manage_account_outbound_proxy" = "Outbound SIP Proxy";
"manage_account_dialog_outbound_proxy_help_message" = "If this field is filled, the outbound proxy will be enabled automatically. Leave it empty to disable it.";
"manage_account_edit_picture" = "Edit picture";
"manage_account_international_prefix" = "International Prefix";
"manage_account_no_device" = "No device found…";
@ -390,21 +438,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";
@ -415,15 +468,20 @@
"notification_chat_message_reaction_received" = "%@ reacted by %@ to: %@";
"notification_chat_message_received_title" = "Message received";
"notification_missed_call_title" = "Missed call";
"notification_earpiece_enforcement_message" = "Please use the earpiece only. Other audio outputs are disabled.";
"operation_in_progress_overlay" = "Operation in progress, please wait";
"or" = "or";
"password" = "Password";
"pending_notification_for_other_accounts_single" = "1 notification for other account(s)";
"pending_notification_for_other_accounts_multiple" = "%@ notifications for other account(s)";
"Personnalize your profil mode" ="Personnalize your profil mode";
"phone_number" = "Phone number";
"picker_categories" = "Categories";
"qr_code_validated" = "QR code validated";
"recordings_title" = "Recordings";
"recordings_list_empty" = "No recording for the moment…";
"selected_participants_count" = "%@ selected participants";
"settings_advanced_early_media_title" = "Early-media";
"settings_advanced_accept_early_media_title" = "Accept early media";
"settings_advanced_allow_outgoing_early_media_title" = "Allow outgoing early media";
"settings_advanced_audio_codecs_title" = "Audio codecs";
@ -433,10 +491,13 @@
"settings_advanced_download_apply_remote_provisioning" = "Download & apply";
"settings_advanced_input_audio_device_title" = "Default input audio device";
"settings_advanced_media_encryption_mandatory_title" = "Media encryption mandatory";
"settings_advanced_create_e2e_encrypted_conferences_title" = "Create end-to-end encrypted meetings & group calls";
"settings_advanced_output_audio_device_title" = "Default output audio device";
"settings_advanced_remote_provisioning_url" = "Remote provisioning URL";
"settings_advanced_title" = "Advanced settings";
"settings_advanced_upload_server_url" = "File sharing server URL";
"settings_advanced_logs_upload_server_url" = "Logs sharing server URL";
"settings_advanced_calls" = "Advanced calls settings";
"settings_advanced_video_codecs_title" = "Video codecs";
"settings_calls_adaptive_rate_control_title" = "Adaptive rate control";
"settings_calls_auto_record_title" = "Automatically start recording calls";
@ -466,23 +527,47 @@
"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_hide_message_content_in_notif_title" = "Do not show message content in iOS notification";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Mark conversation as read when dismissing message notification";
"settings_conversations_title" = "Conversations";
"settings_developer_title" = "Developer settings";
"settings_developer_show_title" = "Show developer settings";
"settings_developer_two_more_clicks_required_toast" = "Click 2 more times to enable developer settings";
"settings_developer_one_more_click_required_toast" = "Click 1 more time to enable developer settings";
"settings_developer_enabled_toast" = "Developer settings enabled";
"settings_developer_already_enabled_toast" = "Developer settings already enabled";
"settings_developer_enable_vu_meters_title" = "Enable record/playback volume vu meters while in call";
"settings_developer_enable_advanced_call_stats_title" = "Show advanced call statistics";
"settings_developer_push_compatible_domains_list_title" = "List of push notifications compatible domains (comma separated)";
"settings_developer_clear_native_friends_in_database_title" = "Clear imported contacts from native address book";
"settings_developer_clear_native_friends_in_database_subtitle" = "They will be imported again the next time the app starts unless you remove the contacts permission";
"settings_developer_cleared_native_friends_in_database_toast" = "Imported contacts have been deleted";
"settings_developer_clear_orphan_auth_info_title" = "Clear authentication info no longer associated to any account";
"settings_developer_no_auth_info_removed_toast" = "No orphan authentication info found";
"settings_developer_cleared_auth_info_toast" = "Orphaned authentication info removed";
"settings_meetings_default_layout_title" = "Default layout";
"settings_meetings_layout_active_speaker_label" = "Active speaker";
"settings_meetings_layout_mosaic_label" = "Mosaic";
"settings_meetings_title" = "Meetings";
"settings_meetings_show_past_meetings_title" = "Show past meetings";
"settings_network_allow_ipv6" = "Allow IPv6";
"settings_network_title" = "Network";
"settings_network_use_wifi_only" = "Use only Wi-Fi networks";
@ -510,6 +595,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";
@ -529,3 +616,6 @@
"welcome_page_title" = "Welcome";
"You will change this mode later" = "You will change this mode later";
"ZRTP" = "ZRTP";
"early_push_unknown_caller" = "Unknown";
"early_push_missed_call_title" = "Missed call";
"early_push_missed_call_body" = "You received a call while your device was locked. Please unlock and reopen the app.";

View file

@ -0,0 +1,163 @@
"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";
"notification_earpiece_enforcement_message" = "Utilice únicamente el auricular. Las demás salidas de audio están desactivadas.";
"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";
"settings_calls_adaptive_rate_control_title" = "Control de velocidad adaptativo";

View file

@ -0,0 +1,17 @@
"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";
"notification_earpiece_enforcement_message" = "Mesedez, erabili entzungailua soilik. Beste audio-irteerak desgaituta daude.";

View file

@ -0,0 +1,32 @@
"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";
"notification_earpiece_enforcement_message" = "Käytä vain kuuloketta. Muut äänilähdöt on poistettu käytöstä.";
"ZRTP" = "ZRTP";
"[https://sip.linphone.org](https://sip.linphone.org)" = "[https://sip.linphone.org](https://sip.linphone.org)";

View file

@ -47,11 +47,14 @@
"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";
"account_settings_push_notification_title" = "Activer les notifications";
"account_settings_sip_proxy_url_title" = "URL du serveur mandataire";
"account_settings_sip_proxy_url_title" = "Registrar URI";
"account_settings_outbound_proxy_title" = "URI du proxy SIP sortant";
"account_settings_stun_server_url_title" = "URL du serveur STUN/TURN";
"account_settings_title" = "Paramètres de compte";
"account_settings_turn_password_title" = "Mot de passe TURN";
@ -93,6 +96,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";
@ -127,7 +131,6 @@
"call_history_deleted_toast" = "Historique supprimé";
"call_not_encrypted" = "Appel non chiffré";
"call_outgoing" = "Appel sortant";
"call_srtp_point_to_point_encrypted" = "Appel chiffré de point à point";
"call_state_connected" = "Actif";
"call_state_paused" = "En pause";
"call_state_paused_by_remote" = "Mis en pause par le correspondant";
@ -135,12 +138,21 @@
"call_stats_audio_title" = "Audio";
"call_stats_media_encryption_title" = "Chiffrement du média";
"call_stats_video_title" = "Video";
"call_transfer_current_call_title" = "Transférer lappel";
"call_transfer_current_call_title" = "Transférer %@ à…";
"call_transfer_active_calls_label" = "Appels en cours";
"call_transfer_no_active_call_label" = "Pas d'autre appel";
"call_transfer_confirm_dialog_tittle" = "Confirmer le transfert";
"call_transfer_confirm_dialog_message" = "Vous allez transférer %1$@ à %2$@.";
"call_transfer_failed_toast" = "Echec du transfert";
"call_transfer_in_progress_toast" = "Transfert en cours";
"call_transfer_successful_toast" = "Appel transféré";
"call_waiting_for_encryption_info" = "En attente du chiffrement…";
"call_conference_end_to_end_encrypted" = "Conférence chiffrée de bout en bout";
"call_zrtp_point_to_point_encrypted" = "Appel chiffré de point à point";
"call_srtp_point_to_point_encrypted" = "Appel chiffré de point à point";
"call_zrtp_end_to_end_encrypted" = "Appel chiffré de bout en bout";
"call_zrtp_sas_validation_required" = "Vérification nécessaire";
"call_zrtp_sas_validation_skip" = "Passer";
"calls_count_label" = "%@ appels";
@ -169,8 +181,15 @@
"contact_details_numbers_and_addresses_title" = "Coordonnées";
"contact_details_remove_from_favourites" = "Retirer des favoris";
"contact_details_share" = "Partager";
"contact_dialog_delete_message" = "Ce contact sera définitivement supprimé.";
"contact_details_trust_title" = "Confiance";
"contact_details_no_device_found" = "Aucun appareil trouvé";
"contact_details_trusted_devices_count" = "Appareils du contact :";
"contact_dialog_increase_trust_level_title" = "Vérifier l'appareil ?";
"contact_dialog_increase_trust_level_message" = "Voulez-vous appeler lappareil %2$@ de %1$@ ?\nVoulez-vous passer lappel ?";
"contact_dialog_devices_trust_help_title" = "Niveau de confiance";
"contact_dialog_devices_trust_help_message" = "Vérifiez les appareils de votre contact pour confirmer que vos communications seront sécurisées et sans compromission.\nQuand tous seront vérifiés, vous atteindrez le niveau de confiance maximal.";
"contact_dialog_delete_title" = "Supprimer %@ ?";
"contact_dialog_delete_message" = "Ce contact sera définitivement supprimé.";
"contact_dialog_pick_phone_number_or_sip_address_title" = "Choisissez un numéro ou adresse SIP";
"contact_edit_title" = "Modifier contact";
"contact_editor_company" = "Entreprise";
@ -179,6 +198,8 @@
"contact_editor_first_name" = "Prénom";
"contact_editor_job_title" = "Poste";
"contact_editor_last_name" = "Nom de famille";
"contact_make_call_check_device_trust" = "Vérifier";
"contact_device_without_name" = "Appareil sans nom";
"contact_message_action" = "Message";
"contact_new_title" = "Nouveau contact";
"contact_video_call_action" = "Appel vidéo";
@ -187,6 +208,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 +222,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" = "Supprimer pour moi";
"conversation_dialog_delete_for_everyone_label" = "Supprimer 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.";
@ -240,20 +269,34 @@
"conversation_info_participant_is_admin_label" = "Administrateur";
"conversation_info_participants_list_title" = "Participants (%d)";
"conversation_invalid_participant_due_to_security_mode_toast" = "Pour des raisons de sécurité, la création d'une conversation avec un participant d'un domaine tiers est désactivé.";
"conversation_menu_configure_ephemeral_messages" = "Messages éphémères";
"conversation_menu_search_in_messages" = "Chercher";
"conversation_menu_go_to_info" = "Informations";
"conversation_menu_configure_ephemeral_messages" = "Messages éphémères";
"conversation_menu_media_files" = "Médias";
"conversation_menu_documents_files" = "Documents";
"conversation_no_media_found" = "Aucun média pour le moment…";
"conversation_no_document_found" = "Aucun document pour le moment…";
"conversation_media_list_title" = "Médias partagés";
"conversation_document_list_title" = "Documents partagés";
"conversation_details_media_documents_title" = "Médias & documents";
"conversation_message_forward_cancelled_toast" = "Transfert annulé";
"conversation_message_forwarded_toast" = "Message transféré";
"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_search_no_match_found" = "Aucun résultat trouvé";
"conversation_search_results_limit_reached_label" = "Nombre maximal de résultats atteint, affinez votre recherche";
"conversation_text_field_hint" = "Dites quelque chose…";
"conversations_list_empty" = "Aucune conversation pour le moment…";
"conversation_take_picture_label" = "Prendre une photo";
"conversation_pick_file_from_gallery_label" = "Ouvrir la galerie";
"conversation_pick_any_file_label" = "Choisir un fichier";
"conversation_file_cant_be_opened_error_toast" = "Impossible d'ouvrir le fichier!";
"conversation_warning_disabled_because_not_secured_title" = "Cette conversation n'est pas chiffrée !";
"conversation_warning_disabled_because_not_secured_subtitle" = "Les messages ne sont pas chiffrés de bout en bout, assurez-vous de ne pas partager d'informations sensibles !";
"debug_logs_copied_to_clipboard_toast" = "Les journaux ont été ajoutés au presse-papier";
"Default" = "Default";
"default_account_disabled" = "Le compte selectionné est désactivé";
@ -265,8 +308,10 @@
"dialog_continue" = "Continuer";
"dialog_deny" = "Refuser";
"dialog_install" = "Installer";
"dialog_do_not_show_anymore" = "Ne plus me montrer ce message";
"dialog_no" = "Non";
"dialog_ok" = "OK";
"dialog_confirm" = "Confirmer";
"dialog_understood" = "J'ai compris";
"dialog_yes" = "Oui";
"drawer_menu_account_connection_status_cleared" = "Désactivé";
"drawer_menu_account_connection_status_connected" = "Connecté";
@ -333,11 +378,13 @@
"manage_account_dialog_international_prefix_help_message" = "Choisissez votre pays pour permettre à Linphone de faire le lien avec vos contacts.";
"manage_account_dialog_remove_account_message" = "Si vous souhaitez supprimer définitivement votre compte rendez-vous sur : https://sip.linphone.org";
"manage_account_dialog_remove_account_title" = "Se déconnecter du compte ?";
"manage_account_outbound_proxy" = "Proxy SIP sortant";
"manage_account_dialog_outbound_proxy_help_message" = "Si ce champ est rempli, l'outbound proxy sera activé automatiquement. Laissez-le vide pour le désactiver.";
"manage_account_edit_picture" = "Modifier";
"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 +437,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";
@ -415,15 +467,20 @@
"notification_chat_message_reaction_received" = "%@ a réagi par %@ à : %@";
"notification_chat_message_received_title" = "Message reçu";
"notification_missed_call_title" = "Appel manqué";
"notification_earpiece_enforcement_message" = "Veuillez utiliser uniquement l'écouteur. Les autres sorties audio sont désactivées.";
"operation_in_progress_overlay" = "Opération en cours, merci de patienter...";
"or" = "ou";
"password" = "Mot de passe";
"pending_notification_for_other_accounts_single" = "1 notification en attente";
"pending_notification_for_other_accounts_multiple" = "%@ notifications en attente";
"Personnalize your profil mode" = "Personnalize your profil mode";
"phone_number" = "Numéro de téléphone";
"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_early_media_title" = "Early media";
"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";
"settings_advanced_audio_codecs_title" = "Codecs audio";
@ -433,10 +490,13 @@
"settings_advanced_download_apply_remote_provisioning" = "Télécharger & appliquer";
"settings_advanced_input_audio_device_title" = "Périphérique de capture par défaut";
"settings_advanced_media_encryption_mandatory_title" = "Rendre le chiffrement du média obligatoire";
"settings_advanced_create_e2e_encrypted_conferences_title" = "Créer en mode chiffré de bout en bout les réunions et les appels de groupe";
"settings_advanced_output_audio_device_title" = "Périphérique d'écoute par défaut";
"settings_advanced_remote_provisioning_url" = "URL de configuration distante";
"settings_advanced_title" = "Paramètres avancés";
"settings_advanced_upload_server_url" = "URL du serveur de partage de fichier";
"settings_advanced_logs_upload_server_url" = "URL du serveur de partage des logs";
"settings_advanced_calls" = "Paramètres d'appels avancés";
"settings_advanced_video_codecs_title" = "Codecs vidéo";
"settings_calls_adaptive_rate_control_title" = "Contrôle automatique de la qualité";
"settings_calls_auto_record_title" = "Enregistrement automatique des appels";
@ -466,23 +526,48 @@
"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_hide_message_content_in_notif_title" = "Masquer le contenu du message dans la notification iOS";
"settings_conversations_mark_as_read_when_dismissing_notif_title" = "Marquer la conversation comme lue lorsqu'une notification de message est supprimée";
"settings_conversations_title" = "Conversations";
"settings_developer_title" = "Paramètres développeurs";
"settings_developer_show_title" = "Afficher les paramètres développeurs";
"settings_developer_two_more_clicks_required_toast" = "Encore 2 clicks pour activer les paramètres développeurs";
"settings_developer_one_more_click_required_toast" = "Encore 1 click pour activer les paramètres développeurs";
"settings_developer_enabled_toast" = "Paramètres développeurs activés";
"settings_developer_already_enabled_toast" = "Paramètres développeurs déjà activés";
"settings_developer_enable_vu_meters_title" = "Activer l'indicateur des volumes d'enregistrement et de lecture";
"settings_developer_enable_advanced_call_stats_title" = "Afficher plus de statistiques d'appel";
"settings_developer_push_compatible_domains_list_title" = "Liste des domaines qui supportent les notifications poussées (séparés par des virgules)";
"settings_developer_clear_native_friends_in_database_title" = "Supprimer les contacts natifs importés";
"settings_developer_clear_native_friends_in_database_subtitle" = "Ils seront synchronisés à nouveau au prochain démarrage de l'application sauf si vous retirez la permission de lire les contacts";
"settings_developer_cleared_native_friends_in_database_toast" = "Contacts importés supprimés";
"settings_developer_clear_orphan_auth_info_title" = "Supprimer les informations d'authentification orphelines";
"settings_developer_no_auth_info_removed_toast" = "Aucune information d'authentification orpheline trouvée";
"settings_developer_cleared_auth_info_toast_single" = "%@ information d'authentification supprimée";
"settings_developer_cleared_auth_info_toast_multiple" = "%@ informations d'authentification supprimées";
"settings_meetings_default_layout_title" = "Disposition par défaut";
"settings_meetings_layout_active_speaker_label" = "Intervenant actif";
"settings_meetings_layout_mosaic_label" = "Mosaïque";
"settings_meetings_title" = "Réunions";
"settings_meetings_show_past_meetings_title" = "Afficher les réunions passées";
"settings_network_allow_ipv6" = "Autoriser l'IPv6";
"settings_network_title" = "Réseau";
"settings_network_use_wifi_only" = "Se connecter uniquement via le Wi-Fi";
@ -510,6 +595,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";
@ -529,3 +616,6 @@
"welcome_page_title" = "Bienvenue";
"You will change this mode later" = "You will change this mode later";
"ZRTP" = "ZRTP";
"early_push_unknown_caller" = "Inconnu";
"early_push_missed_call_title" = "Appel manqué";
"early_push_missed_call_body" = "Vous avez reçu un appel alors que votre appareil était verrouillé. Veuillez déverrouiller et rouvrir l'application.";

View file

@ -0,0 +1,87 @@
"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";
"notification_earpiece_enforcement_message" = "Kérjük, csak a fülhallgatót használja. A többi hangkimenet le van tiltva.";
"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,2 @@
"notification_earpiece_enforcement_message" = "Ве молиме користете само слушалка. Другите аудио излези се оневозможени.";

View file

@ -0,0 +1,410 @@
"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" = "Conferentiefabriek-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 is 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" = "Deel scherm";
"conference_action_show_participants" = "Deelnemers";
"conference_call_empty" = "Wachten op andere deelnemers…";
"conference_failed_to_create_group_call_toast" = "Groepsoproep aanmaken is 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" = "Deel uitnodiging";
"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" = "Deel";
"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" = "U bent bij de groep gekomen";
"conversation_event_conference_destroyed" = "U heeft 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 is 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" = "Foutopsporingslogs zijn opgeschoond";
"help_troubleshooting_debug_logs_upload_error_toast_message" = "Uploaden van foutopsporingslogs is 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" = "Deel logs";
"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 u uw 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, u ontvangtontvangt geen oproepen of berichten.";
"manage_account_status_connected_summary" = "Dit account is online, iedereen kan u bellen.";
"manage_account_status_failed_summary" = "Verbinding met account is mislukt, controleer uw 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 is 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" = "U 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";
"notification_earpiece_enforcement_message" = "Gebruik alleen de oortelefoon. Andere audio-uitgangen zijn uitgeschakeld.";
"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";
"7" = "7";
"9" = "9";
"assistant_invalid_uri_toast" = "Ongeldige URI";
"[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";
"assistant_forgotten_password" = "Wachtwoord vergeten?";
"assistant_web_platform_link" = "subscribe.linphone.org";
"call_audio_device_type_bluetooth" = "Bluetooth (%@)";
"call_action_attended_transfer" = "Begeleidde overdracht";
"call_can_be_trusted_toast" = "Geauthenticeerd apparaat";
"call_transfer_current_call_title" = "Verbind oproep door";
"call_zrtp_sas_validation_skip" = "Sla over";
"contact_dialog_delete_title" = "Verwijder %@?";
"help_about_title" = "Over Linphone";
"settings_contacts_ldap_search_filter_title" = "Filter";
"[https://sip.linphone.org](https://sip.linphone.org)" = "[https://sip.linphone.org](https://sip.linphone.org)";
"5" = "5";
"6" = "6";
"8" = "8";
"conversation_end_to_end_encrypted_bottom_sheet_link" = "https://linphone.org/en/features/#security";
"welcome_page_subtitle" = "in %@";
"settings_contacts_ldap_search_base_title" = "Zoekbasis (mag niet leeg zijn)";
"calls_count_label" = "%@ oproep";
": %@" = ": %@";
"assistant_third_party_sip_account_create_linphone_account" = "Ik wil liever een account aanmaken";
"conference_name_error" = "Confwrentienaamfout";
"conversation_ephemeral_messages_duration_multiple_days" = "%d dagen";
"help_about_user_guide_subtitle" = "Leer alle functies van de app te beheersen, stap voor stap.";

View file

@ -0,0 +1,71 @@
"assistant_account_register" = "Zarejestruj się";
"assistant_account_creation_wrong_phone_number" = "Zły numer?";
"assistant_account_create" = "Utwórz";
"assistant_account_register_unexpected_error" = "Wystąpił nieoczekiwany błąd, spróbuj ponownie później";
"assistant_account_login_forbidden_error" = "Nieprawidłowa nazwa użytkownika lub hasło";
"assistant_dialog_general_terms_and_privacy_policy_title" = "Ogólne warunki i polityka prywatności";
"assistant_dialog_general_terms_label" = "ogólne warunki";
"assistant_dialog_privacy_policy_label" = "polityka prywatności";
"assistant_dialog_confirm_phone_number_title" = "Potwierdź numer telefonu";
"assistant_login_third_party_sip_account" = "Użyj konta SIP innej firmy";
"assistant_create_account_using_email_on_our_web_platform" = "Załóż konto, podając swój adres e-mail na stronie:";
"assistant_already_have_an_account" = "Masz już konto?";
"assistant_permissions_grant_all_of_them" = "OK";
"assistant_permissions_skip_permissions" = "Zrób to później";
"assistant_qr_code_invalid_toast" = "Nieprawidłowy kod QR!";
"assistant_sip_account_transport_protocol" = "Transport";
"bottom_navigation_contacts_label" = "Kontakty";
"bottom_navigation_calls_label" = "Połączenia";
"bottom_navigation_conversations_label" = "Rozmowy";
"bottom_navigation_meetings_label" = "Spotkania";
"assistant_account_login" = "Login";
"assistant_scan_qr_code" = "Zeskanuj kod QR";
"assistant_third_party_sip_account_warning_ok" = "Zrozumiałem";
"assistant_account_register_push_notification_not_received_error" = "Powiadomienie push z tokenem uwierzytelniającym nie zostało otrzymane w ciągu 5 sekund. Spróbuj ponownie później.";
"assistant_permissions_title" = "Przyznaj uprawnienia";
"drawer_menu_manage_account" = "Zarządzaj profilem";
"assistant_no_account_yet" = "Nie masz jeszcze konta?";
"drawer_menu_account_connection_status_connected" = "Połączono";
"authentication_id" = "Identyfikator uwierzytelnienia (jeśli inny)";
"dialog_accept" = "Akceptuj";
"dialog_call" = "Zadzwoń";
"meeting_waiting_room_cancel" = "Anuluj";
"sip_address_display_name" = "Nazwa wyświetlana";
"account_settings_dialog_invalid_password_hint" = "Hasło";
"contact_call_action" = "Zadzwoń";
"contact_details_delete" = "Usuń";
"conversation_action_call" = "Zadzwoń";
"conversation_action_mark_as_read" = "Zaznacz jako odczytane";
"dialog_cancel" = "Anuluj";
"dialog_continue" = "Kontynuuj";
"dialog_deny" = "Nie zezwalaj";
"dialog_install" = "Instaluj";
"dialog_no" = "Nie";
"dialog_ok" = "OK";
"dialog_yes" = "Tak";
"manage_account_device_remove" = "Usuń";
"menu_delete_selected_item" = "Usuń";
"menu_reply_to_chat_message" = "Odpowiedz";
"next" = "Dalej";
"notification_missed_call_title" = "Nieodebrane połączenie";
"notification_earpiece_enforcement_message" = "Proszę używać tylko słuchawki. Inne wyjścia audio są wyłączone.";
"or" = "lub";
"password" = "Hasło";
"phone_number" = "Numer telefonu";
"settings_advanced_device_id" = "ID urządzenia";
"settings_calls_title" = "Połączenia";
"settings_contacts_carddav_name_title" = "Nazwa wyświetlana";
"settings_contacts_carddav_password_title" = "Hasło";
"settings_contacts_carddav_username_title" = "Nazwa użytkownika";
"settings_contacts_ldap_password_title" = "Hasło";
"settings_contacts_title" = "Kontakty";
"settings_conversations_title" = "Rozmowy";
"settings_meetings_title" = "Spotkania";
"sip_address_copied_to_clipboard_toast" = "Adres SIP został skopiowany do schowka";
"sip_address_domain" = "Domena";
"start" = "Rozpocznij";
"uri_handler_config_success_toast" = "Konfiguracja została pomyślnie zastosowana";
"username" = "Nazwa użytkownika";
"welcome_page_2_title" = "Zabezpieczone";
"welcome_page_3_title" = "Open source";
"welcome_page_title" = "Witamy";

View file

@ -0,0 +1,526 @@
"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";
"notification_earpiece_enforcement_message" = "Use apenas o auricular. As outras saídas de áudio estão desativadas.";
"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,9 @@
"[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)";
": %@" = ": %@";
"notification_earpiece_enforcement_message" = "Use apenas o auricular. As outras saídas de áudio estão desativadas.";

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" = "Подключен";
@ -292,6 +292,7 @@
"new_conversation_title" = "Новая беседа";
"next" = "Следующий";
"notification_missed_call_title" = "Пропущенный звонок";
"notification_earpiece_enforcement_message" = "Пожалуйста, используйте только динамик телефона. Другие аудиовыходы отключены.";
"operation_in_progress_overlay" = "Операция выполняется, пожалуйста, подождите";
"or" = "или";
"password" = "Пароль";
@ -362,3 +363,38 @@
"username" = "Имя пользователя";
"conversation_end_to_end_encrypted_event_title" = "Сквозное шифрование беседы";
"conversation_end_to_end_encrypted_event_subtitle" = "Сообщения в этой беседе зашифрованы end-to-end шифрованием. Расшифровать их может только ваш собеседник.";
"authentication_id" = "Идентификатор аутентификации (если отличается)";
"settings_contacts_ldap_search_filter_title" = "База поиска (не может быть пустой)";
"[linphone.org/contact](https://linphone.org/contact)" = "[linphone.org/contact](https://linphone.org/contact)";
"*" = "*";
"**%@**" = "**%@**";
"#" = "#";
"%@" = "%@";
"%lld" = "%lld";
"+" = "+";
"|" = "|";
"❤️" = "❤️";
"👍" = "👍";
"😂" = "😂";
"😢" = "😢";
"😮" = "😮";
"0" = "0";
"1" = "1";
"2" = "2";
"3" = "3";
"5" = "5";
"6" = "6";
"7" = "7";
"8" = "8";
"9" = "9";
"assistant_account_creation_sms_confirmation_explanation" = "Мы отправили код верификации на ваш номер телефона %@.

Пожалуйста, введите код верификации ниже:";
"assistant_dialog_confirm_phone_number_message" = "Вы уверены что хотите использовать телефонный номер %@?";
"assistant_forgotten_password" = "Забыли пароль?";
"assistant_invalid_uri_toast" = "Неверный URI";
"assistant_permissions_read_contacts_title" = "**Доступ к контактам:** Чтобы отображать ваши контакты и находить тех, кто использует %@.";
": %@" = ": %@";
"[https://sip.linphone.org](https://sip.linphone.org)" = "[https://sip.linphone.org](https://sip.linphone.org)";
"4" = "4";
"account_settings_dialog_invalid_password_message" = "Не удалось подключиться, поскольку данные авторизации отсутствуют или неверны для аккаунта\n%@.\n\nВы можете ввести пароль снова или проверить настройки аккаунта.";
"assistant_dialog_general_terms_and_privacy_policy_message" = "Продолжая, вы принимаете наше %@ и %@.";
"assistant_permissions_access_camera_title" = "**Доступ к камере:** Для видеозвонков и видеоконференций.";

View file

@ -0,0 +1,385 @@
"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";
"notification_earpiece_enforcement_message" = "Používajte iba slúchadlo. Ostatné zvukové výstupy sú zakázané.";
"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";
"authentication_id" = "ID pre overenie (ak je odlišné)";
"settings_contacts_ldap_search_filter_title" = "Počiatočný bod hľadania (nesmie byť prázdne)";

View file

@ -199,6 +199,7 @@
"new_conversation_title" = "Нова розмова";
"next" = "Наступний";
"notification_missed_call_title" = "Пропущений виклик";
"notification_earpiece_enforcement_message" = "Будь ласка, використовуйте лише навушник. Інші аудіовиходи вимкнено.";
"operation_in_progress_overlay" = "Операція триває, будь ласка, зачекайте";
"password" = "Пароль";
"phone_number" = "Номер телефону";
@ -377,7 +378,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 +457,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 +523,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" = "已连接";
@ -287,6 +287,7 @@
"new_conversation_title" = "新聊天";
"next" = "下一个";
"notification_missed_call_title" = "未接来电";
"notification_earpiece_enforcement_message" = "请仅使用听筒。其他音频输出已被禁用。";
"operation_in_progress_overlay" = "操作正在进行中,请稍候";
"or" = "或";
"password" = "密码";
@ -362,3 +363,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不能为空 过滤";

Binary file not shown.

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,25 @@
import SwiftUI
struct SplashScreen: View {
var showSpinner: Bool = false
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)
if showSpinner {
ProgressView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea(.all)
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import PushKit
import CallKit
import UserNotifications
class EarlyPushkitDelegate: NSObject, PKPushRegistryDelegate, CXProviderDelegate {
private var activeCalls: [UUID: CXProvider] = [:]
func providerDidReset(_ provider: CXProvider) {}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
Log.info("[EarlyPushkitDelegate] User tried to answer, ending call as device is locked")
action.fail()
provider.reportCall(with: action.callUUID, endedAt: .init(), reason: .unanswered)
activeCalls.removeValue(forKey: action.callUUID)
postMissedCallNotification()
}
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
Log.info("[EarlyPushkitDelegate] Received push credentials, ignoring until core is ready")
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
Log.info("[EarlyPushkitDelegate] Received incoming push while core is not ready, reporting call to CallKit")
let providerConfig = CXProviderConfiguration()
providerConfig.supportsVideo = false
let provider = CXProvider(configuration: providerConfig)
provider.setDelegate(self, queue: .main)
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: NSLocalizedString("early_push_unknown_caller", comment: ""))
update.hasVideo = false
let uuid = UUID()
activeCalls[uuid] = provider
provider.reportNewIncomingCall(with: uuid, update: update) { error in
if let error = error {
Log.error("[EarlyPushkitDelegate] Failed to report call to CallKit: \(error.localizedDescription)")
}
completion()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in
guard let self = self, let provider = self.activeCalls.removeValue(forKey: uuid) else { return }
Log.info("[EarlyPushkitDelegate] Ending unanswered call after timeout")
provider.reportCall(with: uuid, endedAt: .init(), reason: .unanswered)
self.postMissedCallNotification()
}
}
private func postMissedCallNotification() {
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("early_push_missed_call_title", comment: "")
content.body = NSLocalizedString("early_push_missed_call_body", comment: "")
content.sound = .default
let request = UNNotificationRequest(identifier: "early_push_missed_call", content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
Log.error("[EarlyPushkitDelegate] Failed to post missed call notification: \(error.localizedDescription)")
}
}
}
}

View file

@ -102,7 +102,7 @@ class TelecomManager: ObservableObject {
Log.info("Can not start a call with null address!")
return
}
if TelecomManager.callKitEnabled(core: core) {// && !nextCallIsTransfer != true {
let uuid = UUID()
let name = addr?.asStringUriOnly() ?? "Unknown"
@ -167,22 +167,24 @@ class TelecomManager: ObservableObject {
}
func doCallOrJoinConf(address: Address, isVideo: Bool = false, isConference: Bool = false) {
if address.asStringUriOnly().hasPrefix("sip:conference-focus@sip.linphone.org") {
do {
let meetingAddress = try Factory.Instance.createAddress(addr: address.asStringUriOnly())
DispatchQueue.main.async {
withAnimation {
self.meetingWaitingRoomDisplayed = true
self.meetingWaitingRoomSelected = meetingAddress
}
}
} catch {}
} else {
doCallWithCore(
addr: address, isVideo: isVideo, isConference: isConference
)
}
CoreContext.shared.doOnCoreQueue { core in
if let _ = core.findConferenceInformationFromUri(uri: address) {
do {
let meetingAddress = try Factory.Instance.createAddress(addr: address.asStringUriOnly())
DispatchQueue.main.async {
withAnimation {
self.meetingWaitingRoomDisplayed = true
self.meetingWaitingRoomSelected = meetingAddress
}
}
} catch {}
} else {
self.doCallWithCore(
addr: address, isVideo: isVideo, isConference: isConference
)
}
}
}
func doCallWithCore(addr: Address, isVideo: Bool, isConference: Bool) {
@ -195,17 +197,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 +235,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 +290,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 +340,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)")
}
}
}
@ -385,6 +385,13 @@ class TelecomManager: ObservableObject {
}
}
static func isAudioRouteAllowedForCall() -> Bool {
guard AppServices.corePreferences.onlyAllowEarpieceDuringCall else { return true }
let output = AVAudioSession.sharedInstance().currentRoute.outputs.first
Log.info("Current audio route output is \(output?.portType.rawValue ?? "Unknown")")
return output?.portType == .builtInReceiver
}
static func callKitEnabled(core: Core) -> Bool {
#if !targetEnvironment(simulator)
return core.callkitEnabled
@ -435,6 +442,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()
@ -494,38 +502,36 @@ class TelecomManager: ObservableObject {
let isRecordingByRemoteTmp = call.remoteParams?.isRecording ?? false
if isRecordingByRemoteTmp && ToastViewModel.shared.toastMessage.isEmpty {
var displayName = ""
let friend = ContactsManager.shared.getFriendWithAddress(address: call.remoteAddress!)
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
displayName = friend!.address!.displayName!
} else {
if call.remoteAddress!.displayName != nil {
displayName = call.remoteAddress!.displayName!
} else if call.remoteAddress!.username != nil {
displayName = call.remoteAddress!.username!
} else {
displayName = String(call.remoteAddress!.asStringUriOnly().dropFirst(4))
}
}
DispatchQueue.main.async {
self.isRecordingByRemote = isRecordingByRemoteTmp
ToastViewModel.shared.toastMessage = "\(displayName) is recording"
ToastViewModel.shared.displayToast = true
}
Log.info("[Call] Call is recording by \(call.remoteAddress!.asStringUriOnly())")
let displayName: String
let friend = ContactsManager.shared.getFriendWithAddress(address: call.remoteAddress)
if let name = friend?.address?.displayName {
displayName = name
} else if let name = call.remoteAddress?.displayName {
displayName = name
} else if let username = call.remoteAddress?.username {
displayName = username
} else if let uri = call.remoteAddress?.asStringUriOnly() {
displayName = String(uri.dropFirst(4))
} else {
displayName = "Unknown"
}
if !isRecordingByRemoteTmp && ToastViewModel.shared.toastMessage.contains("is recording") {
DispatchQueue.main.async {
self.isRecordingByRemote = isRecordingByRemoteTmp
ToastViewModel.shared.toastMessage = ""
ToastViewModel.shared.displayToast = false
DispatchQueue.main.async {
self.isRecordingByRemote = isRecordingByRemoteTmp
if isRecordingByRemoteTmp {
ToastViewModel.shared.show("\(displayName) is recording")
} else if let toast = ToastViewModel.shared.toast,
toast.message.contains("is recording") {
ToastViewModel.shared.hide()
}
Log.info("[Call] Recording is stopped by \(call.remoteAddress!.asStringUriOnly())")
}
if isRecordingByRemoteTmp {
Log.info("[Call] Call is recording by \(call.remoteAddress?.asStringUriOnly() ?? "")")
} else {
Log.info("[Call] Recording is stopped by \(call.remoteAddress?.asStringUriOnly() ?? "")")
}
if cstate == Call.State.PausedByRemote {
@ -623,6 +629,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
}
@ -648,8 +659,12 @@ class TelecomManager: ObservableObject {
do {
try core.setVideodevice(newValue: "AV Capture: com.apple.avfoundation.avcapturedevice.built-in_video:1")
} catch _ {
}
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ["linphone-earpiece-enforcement"])
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ["linphone-earpiece-enforcement"])
withAnimation {
self.outgoingCallStarted = false
self.callInProgress = false
@ -724,6 +739,10 @@ class TelecomManager: ObservableObject {
}
case .Released:
TelecomManager.setAppData(sCall: call, appData: nil)
if core.callsNb == 0 {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ["linphone-earpiece-enforcement"])
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ["linphone-earpiece-enforcement"])
}
case .Referred:
referedFromCall = call.callLog?.callId
default:

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,84 @@ 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 isSecured = true
@State private var advancedSettingsIsOpen = false
@State private var isShowOutboundProxyPopup = false
@FocusState var isNameFocused: Bool
@FocusState var isPasswordFocused: Bool
@FocusState var isDomainFocused: Bool
@FocusState var isDisplayNameFocused: Bool
@FocusState var isAuthIdFocused: Bool
@FocusState var isSipProxyUrlFocused: 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
ZStack {
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)
}
}
}
}
if isShowOutboundProxyPopup {
PopupView(
isShowPopup: $isShowOutboundProxyPopup,
title: Text("manage_account_outbound_proxy"),
content: Text("manage_account_dialog_outbound_proxy_help_message"),
titleFirstButton: nil,
actionFirstButton: {},
titleSecondButton: Text("dialog_understood"),
actionSecondButton: { self.isShowOutboundProxyPopup.toggle() },
titleThirdButton: nil,
actionThirdButton: {}
)
.padding(.bottom, keyboard.currentHeight)
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
self.isShowOutboundProxyPopup.toggle()
}
}
}
}
}
@ -208,6 +267,112 @@ 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)
}
.padding(.bottom)
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.sipProxyUrl)
.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(isSipProxyUrlFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1)
)
.focused($isSipProxyUrlFocused)
}
.padding(.bottom)
VStack(alignment: .leading) {
HStack {
Text("account_settings_outbound_proxy_title")
.default_text_style_700(styleSize: 15)
.padding(.bottom, -5)
.frame(maxWidth: .infinity, alignment: .leading)
Button(action: {
self.isShowOutboundProxyPopup = true
}, label: {
Image("info")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25)
})
.padding(.trailing, 10)
}
.padding(.bottom, -5)
TextField("account_settings_outbound_proxy_title", text: $accountLoginViewModel.outboundProxy)
.id(3)
.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 +406,7 @@ struct ThirdPartySipAccountLoginFragment: View {
.clipped()
}
.frame(minHeight: geometry.size.height)
.padding(.bottom, keyboard.currentHeight)
}
}

View file

@ -29,6 +29,9 @@ 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 sipProxyUrl: String = ""
@Published var outboundProxy: String = ""
private var mCoreDelegate: CoreDelegate!
@ -39,8 +42,7 @@ class AccountLoginViewModel: ObservableObject {
guard self.coreContext.networkStatusIsConnected else {
DispatchQueue.main.async {
self.coreContext.loggingInProgress = false
ToastViewModel.shared.toastMessage = "Unavailable_network"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Unavailable_network")
}
return
}
@ -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,35 @@ 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.sipProxyUrl.isEmpty) {
let server = self.sipProxyUrl.starts(with: "sip:") ? self.sipProxyUrl : String("sip:" + self.sipProxyUrl)
serverAddress = try Factory.Instance.createAddress(addr: server)
} else {
serverAddress = try Factory.Instance.createAddress(addr: String("sip:" + self.domain))
}
// We use the Address object to easily set the transport protocol
try address.setTransport(newValue: transport)
try accountParams.setServeraddress(newValue: address)
try serverAddress.setTransport(newValue: transport)
try accountParams.setServeraddress(newValue: serverAddress)
var routeAddress: Address
if (!self.outboundProxy.isEmpty) {
let server = self.outboundProxy.starts(with: "sip:") ? self.outboundProxy : String("sip:" + self.outboundProxy)
routeAddress = try Factory.Instance.createAddress(addr: server)
try routeAddress.setTransport(newValue: transport)
try accountParams.setRoutesaddresses(newValue: [routeAddress])
} else {
try accountParams.setRoutesaddresses(newValue: [])
}
// 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 +138,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 " +
@ -132,7 +150,7 @@ class AccountLoginViewModel: ObservableObject {
}
Log.warn("Registration failed for account \(account.displayName()), deleting it from core")
core.removeAccount(account: account)
core.removeAccountWithData(account: account)
default:
break
}
@ -153,6 +171,8 @@ class AccountLoginViewModel: ObservableObject {
DispatchQueue.main.async {
self.domain = "sip.linphone.org"
self.transportType = "TLS"
self.authId = ""
self.outboundProxy = ""
}
} catch { NSLog(error.localizedDescription) }
@ -181,7 +201,7 @@ class AccountLoginViewModel: ObservableObject {
coreContext.doOnCoreQueue { core in
// To completely remove an Account
if let account = core.defaultAccount {
core.removeAccount(account: account)
core.removeAccountWithData(account: account)
// To remove all accounts use
core.clearAccounts()

View file

@ -75,15 +75,18 @@ class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
core.stop()
try? core.start()
}
ToastViewModel.shared.toastMessage = "Success_qr_code_validated"
ToastViewModel.shared.displayToast = true
DispatchQueue.main.async {
ToastViewModel.shared.show("Success_qr_code_validated")
}
} else {
ToastViewModel.shared.toastMessage = "Invalide URI"
ToastViewModel.shared.displayToast.toggle()
DispatchQueue.main.async {
ToastViewModel.shared.show("Invalide URI")
}
}
} else {
ToastViewModel.shared.toastMessage = "Invalide URI"
ToastViewModel.shared.displayToast.toggle()
DispatchQueue.main.async {
ToastViewModel.shared.show("Invalide URI")
}
}
}
}

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"
@ -168,8 +168,7 @@ class RegisterViewModel: ObservableObject {
if !errorMessage.isEmpty {
DispatchQueue.main.async {
ToastViewModel.shared.toastMessage = "Error: \(errorMessage)"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Error: \(errorMessage)")
}
}
@ -257,7 +256,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
}
}
@ -341,8 +340,7 @@ class RegisterViewModel: ObservableObject {
DispatchQueue.main.async {
self.createInProgress = false
ToastViewModel.shared.toastMessage = "Failed_push_notification_not_received_error"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_push_notification_not_received_error")
}
}
@ -412,7 +410,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
}
@ -430,8 +428,7 @@ class RegisterViewModel: ObservableObject {
Log.error("\(RegisterViewModel.TAG) Account manager services hasn't been initialized!")
DispatchQueue.main.async {
ToastViewModel.shared.toastMessage = "Failed_account_register_unexpected_error"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_account_register_unexpected_error")
}
}
}

View file

@ -337,118 +337,78 @@ struct CallView: View {
.zIndex(1)
if !telecomManager.outgoingCallStarted && telecomManager.callInProgress {
if callViewModel.isMediaEncrypted && callViewModel.isRemoteDeviceTrusted && callViewModel.isZrtp {
HStack {
Image("lock-key")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.blueInfo500)
.frame(width: 15, height: 15, alignment: .leading)
.padding(.leading, 50)
.padding(.top, 35)
// Compute the image, text, and color before the HStack
let encryptionInfo: (image: String, textKey: LocalizedStringKey, color: Color) = {
if callViewModel.isMediaEncrypted && callViewModel.isRemoteDeviceTrusted && callViewModel.isZrtp {
// Encrypted call, ZRTP, device trusted
let key: LocalizedStringKey = {
if callViewModel.isConference {
if callViewModel.isEndToEndEncrypted {
return LocalizedStringKey("call_conference_end_to_end_encrypted")
} else if callViewModel.isZrtp {
return LocalizedStringKey("call_zrtp_point_to_point_encrypted")
} else {
return LocalizedStringKey("call_srtp_point_to_point_encrypted")
}
} else {
if callViewModel.isZrtp {
return LocalizedStringKey("call_zrtp_end_to_end_encrypted")
} else {
return LocalizedStringKey("call_srtp_point_to_point_encrypted")
}
}
}()
return ("lock-key", key, Color.blueInfo500)
Text("call_zrtp_end_to_end_encrypted")
.foregroundStyle(Color.blueInfo500)
.default_text_style_white(styleSize: 12)
.padding(.top, 35)
} else if callViewModel.isMediaEncrypted && !callViewModel.isZrtp {
// Encrypted call, SRTP
return ("lock_simple", LocalizedStringKey("call_srtp_point_to_point_encrypted"), Color.blueInfo500)
Spacer()
}
.onTapGesture {
mediaEncryptedSheet = true
}
.frame(height: topBarHeight)
.padding(.leading, geometry.safeAreaInsets.leading)
.zIndex(1)
} else if callViewModel.isMediaEncrypted && !callViewModel.isZrtp {
HStack {
Image("lock_simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.blueInfo500)
.frame(width: 15, height: 15, alignment: .leading)
.padding(.leading, 50)
.padding(.top, 35)
} else if callViewModel.isMediaEncrypted && (!callViewModel.isRemoteDeviceTrusted && callViewModel.isZrtp) || callViewModel.cacheMismatch {
// ZRTP warning
return ("warning-circle", LocalizedStringKey("call_zrtp_sas_validation_required"), Color.orangeWarning600)
Text("call_srtp_point_to_point_encrypted")
.foregroundStyle(Color.blueInfo500)
.default_text_style_white(styleSize: 12)
.padding(.top, 35)
} else if callViewModel.isNotEncrypted {
// Not encrypted
return ("lock-simple-open", LocalizedStringKey("call_not_encrypted"), .white)
Spacer()
} else {
// Waiting for encryption info
return ("progress", LocalizedStringKey("call_waiting_for_encryption_info"), .white)
}
.onTapGesture {
mediaEncryptedSheet = true
}
.frame(height: topBarHeight)
.padding(.leading, geometry.safeAreaInsets.leading)
.zIndex(1)
} else if callViewModel.isMediaEncrypted && (!callViewModel.isRemoteDeviceTrusted && callViewModel.isZrtp) || callViewModel.cacheMismatch {
HStack {
Image("warning-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeWarning600)
.frame(width: 15, height: 15, alignment: .leading)
.padding(.leading, 50)
.padding(.top, 35)
Text("call_zrtp_sas_validation_required")
.foregroundStyle(Color.orangeWarning600)
.default_text_style_white(styleSize: 12)
.padding(.top, 35)
Spacer()
}
.onTapGesture {
mediaEncryptedSheet = true
}
.frame(height: topBarHeight)
.padding(.leading, geometry.safeAreaInsets.leading)
.zIndex(1)
} else if callViewModel.isNotEncrypted {
HStack {
Image("lock_simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 15, height: 15, alignment: .leading)
.padding(.leading, 50)
.padding(.top, 35)
Text("call_not_encrypted")
.foregroundStyle(.white)
.default_text_style_white(styleSize: 12)
.padding(.top, 35)
Spacer()
}
.onTapGesture {
mediaEncryptedSheet = true
}
.frame(height: topBarHeight)
.padding(.leading, geometry.safeAreaInsets.leading)
.zIndex(1)
} else {
HStack {
}()
HStack {
if encryptionInfo.image == "progress" {
ProgressView()
.controlSize(.mini)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.progressViewStyle(CircularProgressViewStyle(tint: encryptionInfo.color))
.frame(width: 15, height: 15, alignment: .leading)
.padding(.leading, 50)
.padding(.top, 35)
Text("call_waiting_for_encryption_info")
.foregroundStyle(.white)
.default_text_style_white(styleSize: 12)
} else {
Image(encryptionInfo.image)
.renderingMode(.template)
.resizable()
.foregroundStyle(encryptionInfo.color)
.frame(width: 15, height: 15, alignment: .leading)
.padding(.leading, 50)
.padding(.top, 35)
Spacer()
}
.frame(height: topBarHeight)
.padding(.leading, geometry.safeAreaInsets.leading)
.zIndex(1)
Text(encryptionInfo.textKey)
.foregroundStyle(encryptionInfo.color)
.default_text_style_white(styleSize: 12)
.padding(.top, 35)
Spacer()
}
.onTapGesture {
mediaEncryptedSheet = true
}
.frame(height: topBarHeight)
.padding(.leading, geometry.safeAreaInsets.leading)
.zIndex(1)
}
}
.frame(height: topBarHeight)
@ -526,8 +486,10 @@ struct CallView: View {
.padding(.top)
.default_text_style_white(styleSize: 22)
Text(callViewModel.remoteAddressCleanedString)
.default_text_style_white_300(styleSize: 16)
if !AppServices.corePreferences.hideSipAddresses {
Text(callViewModel.remoteAddressCleanedString)
.default_text_style_white_300(styleSize: 16)
}
Spacer()
}
@ -558,6 +520,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 +551,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)
@ -636,9 +607,9 @@ struct CallView: View {
}
} else if callViewModel.isConference && !telecomManager.outgoingCallStarted && callViewModel.activeSpeakerParticipant != nil {
let heightValue = (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom)
if optionsChangeLayout == 1 && callViewModel.participantList.count <= 5 {
if optionsChangeLayout == 1 && callViewModel.participantList.count <= 5 && callViewModel.activeSpeakerParticipant?.isScreenSharing == false {
mosaicMode(geometry: geometry, height: heightValue)
} else if optionsChangeLayout == 3 {
} else if optionsChangeLayout == 3 && callViewModel.activeSpeakerParticipant?.isScreenSharing == false {
audioOnlyMode(geometry: geometry, height: heightValue)
} else {
activeSpeakerMode(geometry: geometry)
@ -661,8 +632,7 @@ struct CallView: View {
)
DispatchQueue.main.async {
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
}
}, label: {
HStack {
@ -703,6 +673,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 +872,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 +956,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()
@ -1007,7 +990,7 @@ struct CallView: View {
.cornerRadius(20)
ForEach(0..<callViewModel.participantList.count, id: \.self) { index in
if callViewModel.activeSpeakerParticipant != nil && !callViewModel.participantList[index].address.equal(address2: callViewModel.activeSpeakerParticipant!.address) {
if callViewModel.activeSpeakerParticipant != nil && (!callViewModel.participantList[index].address.weakEqual(address2: callViewModel.activeSpeakerParticipant!.address) || callViewModel.activeSpeakerParticipant!.isScreenSharing) {
ZStack {
if callViewModel.participantList[index].isJoining {
VStack {
@ -1057,7 +1040,7 @@ struct CallView: View {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
if index < callViewModel.participantList.count {
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.weakEqual(address2: callViewModel.participantList[index].address)})
if participantVideo != nil && participantVideo!.devices.first != nil {
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
@ -1143,6 +1126,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()
@ -1172,7 +1160,7 @@ struct CallView: View {
.cornerRadius(20)
ForEach(0..<callViewModel.participantList.count, id: \.self) { index in
if callViewModel.activeSpeakerParticipant != nil && !callViewModel.participantList[index].address.equal(address2: callViewModel.activeSpeakerParticipant!.address) {
if callViewModel.activeSpeakerParticipant != nil && (!callViewModel.participantList[index].address.weakEqual(address2: callViewModel.activeSpeakerParticipant!.address) || callViewModel.activeSpeakerParticipant!.isScreenSharing) {
ZStack {
if callViewModel.participantList[index].isJoining {
VStack {
@ -1222,7 +1210,7 @@ struct CallView: View {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
if index < callViewModel.participantList.count {
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.weakEqual(address2: callViewModel.participantList[index].address)})
if participantVideo != nil && participantVideo!.devices.first != nil {
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
@ -1362,6 +1350,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)
@ -1473,7 +1466,7 @@ struct CallView: View {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
if index < callViewModel.participantList.count {
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.weakEqual(address2: callViewModel.participantList[index].address)})
if participantVideo != nil && participantVideo!.devices.first != nil {
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
@ -1598,6 +1591,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)
@ -1709,7 +1707,7 @@ struct CallView: View {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
if index < callViewModel.participantList.count {
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.equal(address2: callViewModel.participantList[index].address)})
let participantVideo = core.currentCall?.conference?.participantList.first(where: {$0.address!.weakEqual(address2: callViewModel.participantList[index].address)})
if participantVideo != nil && participantVideo!.devices.first != nil {
participantVideo!.devices.first!.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
@ -1917,36 +1915,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()
@ -1964,38 +1964,46 @@ struct CallView: View {
.background(callViewModel.micMutted ? Color.redDanger500 : Color.gray500)
.cornerRadius(40)
Button {
if AVAudioSession.sharedInstance().availableInputs != nil
&& !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
audioRouteSheet = true
}
} else {
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? .speaker : .none)
} catch _ {
}
}
} label: {
HStack {
Image(imageAudioRoute)
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
.onAppear(perform: getAudioRouteImage)
.onReceive(pub) { _ in
self.getAudioRouteImage()
if !callViewModel.hasAudioRouteRestriction {
Button {
if AVAudioSession.sharedInstance().availableInputs != nil
&& !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
audioRouteSheet = true
}
} else {
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? .speaker : .none)
} catch _ {
}
}
} label: {
HStack {
Image(imageAudioRoute)
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle(buttonSize: buttonSize))
.frame(width: buttonSize, height: buttonSize)
.background(Color.gray500)
.cornerRadius(40)
}
.buttonStyle(PressedButtonStyle(buttonSize: buttonSize))
.frame(width: buttonSize, height: buttonSize)
.background(Color.gray500)
.cornerRadius(40)
Color.clear
.frame(width: 0, height: 0)
.onAppear {
getAudioRouteImage()
callViewModel.enforceEarpieceIfNeeded()
}
.onReceive(pub) { _ in
self.getAudioRouteImage()
callViewModel.enforceEarpieceIfNeeded()
}
}
.frame(height: geo.size.height * 0.15)
.padding(.horizontal, 20)
@ -2005,22 +2013,18 @@ struct CallView: View {
HStack(spacing: 0) {
if callViewModel.isOneOneCall {
VStack {
Button {
if callViewModel.callsCounter < 2 {
withAnimation {
callViewModel.isTransferInsteadCall = true
isShowStartCallFragment.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
telecomManager.callStarted = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
telecomManager.callStarted = true
}
}
} else {
callViewModel.transferClicked()
}
Button {
withAnimation {
callViewModel.isTransferInsteadCall = true
isShowStartCallFragment.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
telecomManager.callStarted = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
telecomManager.callStarted = true
}
}
} label: {
HStack {
Image("phone-transfer")
@ -2035,7 +2039,7 @@ struct CallView: View {
.background(Color.gray500)
.cornerRadius(40)
Text(callViewModel.callsCounter < 2 ? "call_action_blind_transfer" : "call_action_attended_transfer")
Text("call_action_blind_transfer")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
@ -2214,34 +2218,43 @@ struct CallView: View {
}
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
} else {
VStack {
Button {
changeLayoutSheet = true
} label: {
HStack {
Image("notebook")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle(buttonSize: buttonSize))
.frame(width: buttonSize, height: buttonSize)
.background(Color.gray500)
.cornerRadius(40)
Text("call_action_change_layout")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
ZStack {
VStack {
Button {
changeLayoutSheet = true
} label: {
HStack {
Image("layout")
.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.activeSpeakerParticipant?.isScreenSharing == true)
Text("call_action_change_layout")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
if callViewModel.activeSpeakerParticipant?.isScreenSharing == true {
Color.gray600.opacity(0.8)
.allowsHitTesting(false)
}
}
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
}
}
.frame(height: geo.size.height * 0.15)
HStack(spacing: 0) {
if !CorePreferences.disableChatFeature && callViewModel.chatEnabled {
if !AppServices.corePreferences.disableChatFeature && callViewModel.chatEnabled {
VStack {
Button {
callViewModel.createConversation()
@ -2333,7 +2346,7 @@ struct CallView: View {
.frame(width: buttonSize, height: buttonSize)
.background(callViewModel.isRecording ? Color.redDanger500 : Color.gray500)
.cornerRadius(40)
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
.disabled(AppServices.corePreferences.disableCallRecordings || callViewModel.isPaused || telecomManager.isPausedByRemote)
Text("call_action_record_call")
.foregroundStyle(.white)
@ -2341,7 +2354,7 @@ struct CallView: View {
}
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
if telecomManager.isPausedByRemote {
if AppServices.corePreferences.disableCallRecordings || callViewModel.isPaused || telecomManager.isPausedByRemote {
Color.gray600.opacity(0.8)
.allowsHitTesting(false)
}
@ -2401,7 +2414,7 @@ struct CallView: View {
.frame(width: geo.size.width * 0.24, height: geo.size.width * 0.24)
.hidden()
if CorePreferences.disableChatFeature || !callViewModel.chatEnabled {
if AppServices.corePreferences.disableChatFeature || !callViewModel.chatEnabled {
VStack {
Button {
} label: {
@ -2431,22 +2444,18 @@ struct CallView: View {
HStack {
if callViewModel.isOneOneCall {
VStack {
Button {
if callViewModel.callsCounter < 2 {
withAnimation {
callViewModel.isTransferInsteadCall = true
isShowStartCallFragment.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
telecomManager.callStarted = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
telecomManager.callStarted = true
}
}
} else {
callViewModel.transferClicked()
}
Button {
withAnimation {
callViewModel.isTransferInsteadCall = true
isShowStartCallFragment.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
telecomManager.callStarted = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
telecomManager.callStarted = true
}
}
} label: {
HStack {
Image("phone-transfer")
@ -2461,7 +2470,7 @@ struct CallView: View {
.background(Color.gray500)
.cornerRadius(40)
Text(callViewModel.callsCounter < 2 ? "call_action_blind_transfer" : "call_action_attended_transfer")
Text("call_action_blind_transfer")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
@ -2648,7 +2657,7 @@ struct CallView: View {
changeLayoutSheet = true
} label: {
HStack {
Image("notebook")
Image("layout")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
@ -2667,7 +2676,7 @@ struct CallView: View {
.frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125)
}
if !CorePreferences.disableChatFeature && callViewModel.chatEnabled {
if !AppServices.corePreferences.disableChatFeature && callViewModel.chatEnabled {
VStack {
Button {
callViewModel.createConversation()
@ -2759,7 +2768,7 @@ struct CallView: View {
.frame(width: buttonSize, height: buttonSize)
.background(callViewModel.isRecording ? Color.redDanger500 : Color.gray500)
.cornerRadius(40)
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
.disabled(AppServices.corePreferences.disableCallRecordings || callViewModel.isPaused || telecomManager.isPausedByRemote)
Text("call_action_record_call")
.foregroundStyle(.white)
@ -2767,7 +2776,7 @@ struct CallView: View {
}
.frame(width: geo.size.width * 0.125, height: geo.size.width * 0.125)
if callViewModel.isPaused || telecomManager.isPausedByRemote {
if AppServices.corePreferences.disableCallRecordings || callViewModel.isPaused || telecomManager.isPausedByRemote {
Color.gray600.opacity(0.8)
.allowsHitTesting(false)
}

View file

@ -70,72 +70,74 @@ struct AudioRouteBottomSheet: View {
})
.frame(maxHeight: .infinity)
Button(action: {
optionsAudioRoute = 2
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
} catch _ {
}
}, label: {
HStack {
Image(optionsAudioRoute == 2 ? "radio-button-fill" : "radio-button")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("call_audio_device_type_speaker")
.default_text_style_white(styleSize: 15)
Spacer()
Image("speaker-high")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
})
.frame(maxHeight: .infinity)
Button(action: {
optionsAudioRoute = 3
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
try AVAudioSession.sharedInstance().setPreferredInput(
AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first)
} catch _ {
}
}, label: {
HStack {
Image(optionsAudioRoute == 3 ? "radio-button-fill" : "radio-button")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text(String(format: String(localized: "call_audio_device_type_bluetooth"),
AVAudioSession.sharedInstance().currentRoute.outputs.first?.portName ?? ""))
.default_text_style_white(styleSize: 15)
Spacer()
Image("bluetooth")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
})
.frame(maxHeight: .infinity)
if !AppServices.corePreferences.onlyAllowEarpieceDuringCall {
Button(action: {
optionsAudioRoute = 2
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
} catch _ {
}
}, label: {
HStack {
Image(optionsAudioRoute == 2 ? "radio-button-fill" : "radio-button")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("call_audio_device_type_speaker")
.default_text_style_white(styleSize: 15)
Spacer()
Image("speaker-high")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
})
.frame(maxHeight: .infinity)
Button(action: {
optionsAudioRoute = 3
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
try AVAudioSession.sharedInstance().setPreferredInput(
AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first)
} catch _ {
}
}, label: {
HStack {
Image(optionsAudioRoute == 3 ? "radio-button-fill" : "radio-button")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text(String(format: String(localized: "call_audio_device_type_bluetooth"),
AVAudioSession.sharedInstance().currentRoute.outputs.first?.portName ?? ""))
.default_text_style_white(styleSize: 15)
Spacer()
Image("bluetooth")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
})
.frame(maxHeight: .infinity)
}
}
.padding(.horizontal, 20)
.background(Color.gray600)

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

@ -34,8 +34,9 @@ class ParticipantModel: ObservableObject {
@Published var isMuted: Bool
@Published var isAdmin: Bool
@Published var isSpeaking: Bool
@Published var isScreenSharing: Bool
init(address: Address, isJoining: Bool = false, onPause: Bool = false, isMuted: Bool = false, isAdmin: Bool = false, isSpeaking: Bool = false) {
init(address: Address, isJoining: Bool = false, onPause: Bool = false, isMuted: Bool = false, isAdmin: Bool = false, isSpeaking: Bool = false, isScreenSharing: Bool = false) {
self.address = address
self.sipUri = address.asStringUriOnly()
@ -49,6 +50,7 @@ class ParticipantModel: ObservableObject {
self.isMuted = isMuted
self.isAdmin = isAdmin
self.isSpeaking = isSpeaking
self.isScreenSharing = isScreenSharing
ContactsManager.shared.getFriendWithAddressInCoreQueue(address: self.address) { friendResult in
if let addressFriend = friendResult {

View file

@ -22,6 +22,7 @@ import linphonesw
import AVFAudio
import Combine
import SwiftUI
import UserNotifications
// swiftlint:disable line_length
// swiftlint:disable type_body_length
@ -48,6 +49,7 @@ class CallViewModel: ObservableObject {
@Published var zrtpPopupDisplayed: Bool = false
@Published var upperCaseAuthTokenToRead = ""
@Published var upperCaseAuthTokenToListen = ""
@Published var isEndToEndEncrypted: Bool = false
@Published var isMediaEncrypted: Bool = false
@Published var isNotEncrypted: Bool = false
@Published var isZrtp: Bool = false
@ -89,22 +91,142 @@ class CallViewModel: ObservableObject {
@Published var letters4: String = "DD"
@Published var operationInProgress: Bool = false
@Published var hasAudioRouteRestriction: Bool = false
@Published var audioMutedByEarpieceEnforcement: Bool = false
private var mCoreDelegate: CoreDelegate?
private var chatRoomDelegate: ChatRoomDelegate?
private static let earpieceNotificationIdentifier = "linphone-earpiece-enforcement"
private var isEnforcingEarpiece: Bool = false
private var routeChangeObserver: Any?
init() {
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
} catch _ {
}
hasAudioRouteRestriction = AppServices.corePreferences.onlyAllowEarpieceDuringCall
NotificationCenter.default.addObserver(forName: Notification.Name("CallViewModelReset"), object: nil, queue: nil) { notification in
self.resetCallView()
}
if hasAudioRouteRestriction {
routeChangeObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.routeChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.enforceEarpieceIfNeeded()
}
}
}
deinit {
if let observer = routeChangeObserver {
NotificationCenter.default.removeObserver(observer)
}
}
func isRouteAllowed() -> Bool {
guard AppServices.corePreferences.onlyAllowEarpieceDuringCall else { return true }
let output = AVAudioSession.sharedInstance().currentRoute.outputs.first
return output?.portType == .builtInReceiver
}
func enforceEarpieceIfNeeded() {
guard hasAudioRouteRestriction else { return }
if isRouteAllowed() {
if audioMutedByEarpieceEnforcement {
coreContext.doOnCoreQueue { core in
if let call = self.currentCall {
call.microphoneMuted = false
core.micEnabled = true
let micMuttedTmp = call.microphoneMuted || !core.micEnabled
DispatchQueue.main.async {
self.micMutted = micMuttedTmp
self.audioMutedByEarpieceEnforcement = false
}
Log.info("\(CallViewModel.TAG) Earpiece restored, unmuting audio")
}
}
cancelEarpieceNotification()
}
} else {
guard !isEnforcingEarpiece else { return }
isEnforcingEarpiece = true
Log.info("\(CallViewModel.TAG) Disallowed audio route detected, muting and forcing earpiece")
DispatchQueue.main.async {
self.micMutted = true
self.audioMutedByEarpieceEnforcement = true
}
self.forceEarpiece()
self.postEarpieceEnforcementNotification()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
if !self.isRouteAllowed() {
self.forceEarpiece()
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.isEnforcingEarpiece = false
}
}
}
private func forceEarpiece() {
coreContext.doOnCoreQueue { core in
if let call = self.currentCall {
call.microphoneMuted = true
AudioRouteUtils.routeAudioToEarpiece(core: core, call: call)
}
}
do {
let session = AVAudioSession.sharedInstance()
try session.overrideOutputAudioPort(.none)
let receiver = session.availableInputs?.first(where: { $0.portType.rawValue.contains("Receiver") })
?? session.availableInputs?.first
try session.setPreferredInput(receiver)
} catch {
Log.error("\(CallViewModel.TAG) Failed to override audio port to earpiece: \(error)")
}
}
private func postEarpieceEnforcementNotification() {
let content = UNMutableNotificationContent()
content.title = "Linphone"
content.body = String(localized: "notification_earpiece_enforcement_message")
content.sound = .default
let request = UNNotificationRequest(
identifier: CallViewModel.earpieceNotificationIdentifier,
content: content,
trigger: nil
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
Log.error("\(CallViewModel.TAG) Failed to post earpiece notification: \(error)")
}
}
}
private func cancelEarpieceNotification() {
UNUserNotificationCenter.current().removeDeliveredNotifications(
withIdentifiers: [CallViewModel.earpieceNotificationIdentifier]
)
UNUserNotificationCenter.current().removePendingNotificationRequests(
withIdentifiers: [CallViewModel.earpieceNotificationIdentifier]
)
}
func resetCallView() {
cancelEarpieceNotification()
audioMutedByEarpieceEnforcement = false
DispatchQueue.main.async {
self.displayName = ""
self.avatarModel = nil
@ -138,6 +260,7 @@ class CallViewModel: ObservableObject {
}
var displayNameTmp = ""
var isEndToEndEncryptedTmp = false
var isOneOneCallTmp = false
if self.currentCall?.remoteAddress != nil {
@ -147,6 +270,7 @@ class CallViewModel: ObservableObject {
isOneOneCallTmp = true
} else {
displayNameTmp = confInfo?.subject ?? "Conference-focus"
isEndToEndEncryptedTmp = confInfo?.securityLevel == .EndToEnd || conf?.currentParams?.securityLevel == .EndToEnd
}
}
@ -178,7 +302,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 {
@ -253,6 +379,7 @@ class CallViewModel: ObservableObject {
self.videoDisplayed = videoDisplayedTmp
self.isOneOneCall = isOneOneCallTmp
self.isEndToEndEncrypted = isEndToEndEncryptedTmp
self.isMediaEncrypted = isMediaEncryptedTmp
self.isNotEncrypted = false
self.isZrtp = isZrtpTmp
@ -270,7 +397,7 @@ class CallViewModel: ObservableObject {
}
}
self.callDelegate = CallDelegateStub(onEncryptionChanged: { (_: Call, _: Bool, _: String)in
self.callDelegate = CallDelegateStub(onEncryptionChanged: { (_: Call, _: Bool, _: String) in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.updateEncryption(withToast: false)
}
@ -331,9 +458,9 @@ class CallViewModel: ObservableObject {
var myParticipantModelTmp: ParticipantModel?
if conf.me?.address != nil {
myParticipantModelTmp = ParticipantModel(address: conf.me!.address!, isJoining: false, onPause: false, isMuted: false, isAdmin: conf.me!.isAdmin)
myParticipantModelTmp = ParticipantModel(address: conf.me!.address!, isJoining: false, onPause: false, isMuted: false, isAdmin: conf.me!.isAdmin, isScreenSharing: false)
} else if self.currentCall?.callLog?.localAddress != nil {
myParticipantModelTmp = ParticipantModel(address: self.currentCall!.callLog!.localAddress!, isJoining: false, onPause: false, isMuted: false, isAdmin: conf.me!.isAdmin)
myParticipantModelTmp = ParticipantModel(address: self.currentCall!.callLog!.localAddress!, isJoining: false, onPause: false, isMuted: false, isAdmin: conf.me!.isAdmin, isScreenSharing: false)
}
var activeSpeakerParticipantTmp: ParticipantModel?
@ -342,21 +469,24 @@ class CallViewModel: ObservableObject {
address: conf.activeSpeakerParticipantDevice!.address!,
isJoining: conf.activeSpeakerParticipantDevice!.state == .Joining || conf.activeSpeakerParticipantDevice!.state == .Alerting,
onPause: conf.activeSpeakerParticipantDevice!.state == .OnHold,
isMuted: conf.activeSpeakerParticipantDevice!.isMuted
isMuted: conf.activeSpeakerParticipantDevice!.isMuted,
isScreenSharing: conf.activeSpeakerParticipantDevice!.screenSharingEnabled
)
} else if conf.participantList.first?.address != nil && conf.participantList.first!.address!.clone()!.equal(address2: (conf.me?.address)!) {
activeSpeakerParticipantTmp = ParticipantModel(
address: conf.participantDeviceList.first!.address!,
isJoining: conf.participantDeviceList.first!.state == .Joining || conf.participantDeviceList.first!.state == .Alerting,
onPause: conf.participantDeviceList.first!.state == .OnHold,
isMuted: conf.participantDeviceList.first!.isMuted
isMuted: conf.participantDeviceList.first!.isMuted,
isScreenSharing: conf.participantDeviceList.first!.screenSharingEnabled
)
} else if conf.participantList.last?.address != nil {
activeSpeakerParticipantTmp = ParticipantModel(
address: conf.participantDeviceList.last!.address!,
isJoining: conf.participantDeviceList.last!.state == .Joining || conf.participantDeviceList.last!.state == .Alerting,
onPause: conf.participantDeviceList.last!.state == .OnHold,
isMuted: conf.participantDeviceList.last!.isMuted
isMuted: conf.participantDeviceList.last!.isMuted,
isScreenSharing: conf.participantDeviceList.last!.screenSharingEnabled
)
}
@ -387,7 +517,8 @@ class CallViewModel: ObservableObject {
isJoining: participantDevice.state == .Joining || participantDevice.state == .Alerting,
onPause: participantDevice.state == .OnHold,
isMuted: participantDevice.isMuted,
isAdmin: isAdmin ?? false
isAdmin: isAdmin ?? false,
isScreenSharing: participantDevice.screenSharingEnabled
)
)
}
@ -445,7 +576,8 @@ class CallViewModel: ObservableObject {
isJoining: pDevice.state == .Joining || pDevice.state == .Alerting,
onPause: pDevice.state == .OnHold,
isMuted: pDevice.isMuted,
isAdmin: isAdmin ?? false
isAdmin: isAdmin ?? false,
isScreenSharing: pDevice.screenSharingEnabled
)
)
}
@ -461,21 +593,24 @@ class CallViewModel: ObservableObject {
address: conference.activeSpeakerParticipantDevice!.address!,
isJoining: conference.activeSpeakerParticipantDevice!.state == .Joining || conference.activeSpeakerParticipantDevice!.state == .Alerting,
onPause: conference.activeSpeakerParticipantDevice!.state == .OnHold,
isMuted: conference.activeSpeakerParticipantDevice!.isMuted
isMuted: conference.activeSpeakerParticipantDevice!.isMuted,
isScreenSharing: conference.activeSpeakerParticipantDevice!.screenSharingEnabled
)
} else if conference.participantList.first?.address != nil && conference.participantList.first!.address!.clone()!.equal(address2: (conference.me?.address)!) {
activeSpeakerParticipantTmp = ParticipantModel(
address: conference.participantDeviceList.first!.address!,
isJoining: conference.participantDeviceList.first!.state == .Joining || conference.participantDeviceList.first!.state == .Alerting,
onPause: conference.participantDeviceList.first!.state == .OnHold,
isMuted: conference.participantDeviceList.first!.isMuted
isMuted: conference.participantDeviceList.first!.isMuted,
isScreenSharing: conference.participantDeviceList.first!.screenSharingEnabled
)
} else if conference.participantList.last?.address != nil {
activeSpeakerParticipantTmp = ParticipantModel(
address: conference.participantDeviceList.last!.address!,
isJoining: conference.participantDeviceList.last!.state == .Joining || conference.participantDeviceList.last!.state == .Alerting,
onPause: conference.participantDeviceList.last!.state == .OnHold,
isMuted: conference.participantDeviceList.last!.isMuted
isMuted: conference.participantDeviceList.last!.isMuted,
isScreenSharing: conference.participantDeviceList.last!.screenSharingEnabled
)
}
@ -521,7 +656,8 @@ class CallViewModel: ObservableObject {
isJoining: pDevice.state == .Joining || pDevice.state == .Alerting,
onPause: pDevice.state == .OnHold,
isMuted: pDevice.isMuted,
isAdmin: isAdmin ?? false
isAdmin: isAdmin ?? false,
isScreenSharing: pDevice.screenSharingEnabled
)
)
}
@ -570,7 +706,57 @@ class CallViewModel: ObservableObject {
}
}
})
}, onParticipantDeviceIsSpeakingChanged: { (_: Conference, device: ParticipantDevice, isSpeaking: Bool) in
}, onParticipantDeviceScreenSharingChanged: { (_: Conference, device: ParticipantDevice, enabled: Bool) in
self.toggleVideoMode(isAudioOnlyMode: false)
let activeSpeakerParticipantTmp = ParticipantModel(
address: device.address!,
isJoining: device.state == .Joining || device.state == .Alerting,
onPause: device.state == .OnHold,
isMuted: device.isMuted,
isScreenSharing: device.screenSharingEnabled
)
var activeSpeakerNameTmp = ""
let friend = ContactsManager.shared.getFriendWithAddress(address: activeSpeakerParticipantTmp.address)
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
activeSpeakerNameTmp = friend!.address!.displayName!
} else {
if activeSpeakerParticipantTmp.address.displayName != nil {
activeSpeakerNameTmp = activeSpeakerParticipantTmp.address.displayName!
} else if activeSpeakerParticipantTmp.address.username != nil {
activeSpeakerNameTmp = activeSpeakerParticipantTmp.address.username!
} else {
activeSpeakerNameTmp = String(activeSpeakerParticipantTmp.address.asStringUriOnly().dropFirst(4))
}
}
var participantListTmp: [ParticipantModel] = []
conference.participantDeviceList.forEach({ pDevice in
if pDevice.address != nil && !conference.isMe(uri: pDevice.address!.clone()!) {
if !conference.isMe(uri: pDevice.address!.clone()!) {
let isAdmin = conference.participantList.first(where: {$0.address!.equal(address2: pDevice.address!.clone()!)})?.isAdmin
participantListTmp.append(
ParticipantModel(
address: pDevice.address!,
isJoining: pDevice.state == .Joining || pDevice.state == .Alerting,
onPause: pDevice.state == .OnHold,
isMuted: pDevice.isMuted,
isAdmin: isAdmin ?? false,
isScreenSharing: pDevice.screenSharingEnabled
)
)
}
}
})
DispatchQueue.main.async {
self.activeSpeakerParticipant = activeSpeakerParticipantTmp
self.activeSpeakerName = activeSpeakerNameTmp
self.participantList = participantListTmp
}
} , onParticipantDeviceIsSpeakingChanged: { (_: Conference, device: ParticipantDevice, isSpeaking: Bool) in
let isSpeaking = device.isSpeaking
if self.myParticipantModel != nil && self.myParticipantModel!.address.clone()!.equal(address2: device.address!) {
DispatchQueue.main.async {
@ -605,7 +791,8 @@ class CallViewModel: ObservableObject {
address: participantDevice!.address!,
isJoining: participantDevice!.state == .Joining || participantDevice!.state == .Alerting,
onPause: participantDevice!.state == .OnHold,
isMuted: participantDevice!.isMuted
isMuted: participantDevice!.isMuted,
isScreenSharing: participantDevice!.screenSharingEnabled
)
var activeSpeakerNameTmp = ""
@ -636,7 +823,8 @@ class CallViewModel: ObservableObject {
isJoining: pDevice.state == .Joining || pDevice.state == .Alerting,
onPause: pDevice.state == .OnHold,
isMuted: pDevice.isMuted,
isAdmin: isAdmin ?? false
isAdmin: isAdmin ?? false,
isScreenSharing: pDevice.screenSharingEnabled
)
)
}
@ -713,44 +901,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
@ -969,8 +1158,7 @@ class CallViewModel: ObservableObject {
self.isNotEncrypted = false
if isDeviceTrusted && withToast {
ToastViewModel.shared.toastMessage = "Info_call_securised"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Info_call_securised")
}
}
@ -985,14 +1173,13 @@ class CallViewModel: ObservableObject {
self.isNotEncrypted = false
}
case MediaEncryption.None:
let isMediaEncryptedTmp = self.currentCall?.params?.mediaEncryption != .None && self.currentCall?.params?.mediaEncryption != .ZRTP
let isNotEncryptedTmp = self.currentCall?.params?.mediaEncryption == .None
DispatchQueue.main.async {
self.isMediaEncrypted = false
self.isMediaEncrypted = isMediaEncryptedTmp
self.isZrtp = false
if self.currentCall!.state == .StreamsRunning {
self.isNotEncrypted = true
} else {
self.isNotEncrypted = false
}
self.isNotEncrypted = isNotEncryptedTmp
}
}
}
@ -1056,8 +1243,9 @@ class CallViewModel: ObservableObject {
try callToTransferTo!.transferToAnother(dest: self.currentCall!)
Log.info("[CallViewModel] Attended transfer is successful")
} catch _ {
ToastViewModel.shared.toastMessage = "Failed_toast_call_transfer_failed"
ToastViewModel.shared.displayToast = true
DispatchQueue.main.async {
ToastViewModel.shared.show("Failed_toast_call_transfer_failed")
}
Log.error("[CallViewModel] Failed to make attended transfer!")
}
@ -1076,9 +1264,9 @@ class CallViewModel: ObservableObject {
try self.currentCall!.transferTo(referTo: toAddress)
Log.info("[CallViewModel] Blind call transfer is successful")
} catch _ {
ToastViewModel.shared.toastMessage = "Failed_toast_call_transfer_failed"
ToastViewModel.shared.displayToast = true
DispatchQueue.main.async {
ToastViewModel.shared.show("Failed_toast_call_transfer_failed")
}
Log.error("[CallViewModel] Failed to make blind call transfer!")
}
}
@ -1273,8 +1461,7 @@ class CallViewModel: ObservableObject {
)
DispatchQueue.main.async {
self.operationInProgress = false
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
}
}
@ -1296,7 +1483,7 @@ class CallViewModel: ObservableObject {
if let chatParams = params.chatParams {
chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
let sameDomain = remoteAddress?.domain == CorePreferences.defaultDomain && remoteAddress?.domain == account.params?.domain
let sameDomain = remoteAddress?.domain == AppServices.corePreferences.defaultDomain && remoteAddress?.domain == account.params?.domain
if account.params != nil && (account.params!.instantMessagingEncryptionMandatory && sameDomain) {
Log.info(
"\(CallViewModel.TAG) Account is in secure mode & domain matches, requesting E2E encryption"
@ -1364,8 +1551,7 @@ class CallViewModel: ObservableObject {
self.chatRoomDelegate = nil
DispatchQueue.main.async {
self.operationInProgress = false
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
}
}, onConferenceJoined: { (chatRoom: ChatRoom, _: EventLog) in
@ -1399,8 +1585,7 @@ class CallViewModel: ObservableObject {
self.chatRoomDelegate = nil
DispatchQueue.main.async {
self.operationInProgress = false
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
}
})

View file

@ -39,9 +39,9 @@ class MeetingWaitingRoomViewModel: ObservableObject {
init() {
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
} catch _ {
try configureAudio(.call)
} catch {
print("Audio session error: \(error)")
}
if !telecomManager.callStarted {
self.resetMeetingRoomView()
@ -51,9 +51,9 @@ class MeetingWaitingRoomViewModel: ObservableObject {
func resetMeetingRoomView() {
if self.telecomManager.meetingWaitingRoomSelected != nil {
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
} catch _ {
try configureAudio(.call)
} catch {
print("Audio session error: \(error)")
}
coreContext.doOnCoreQueue { core in
@ -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)
@ -212,9 +212,9 @@ class MeetingWaitingRoomViewModel: ObservableObject {
func enableAVAudioSession() {
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch _ {
try configureAudio(.call)
} catch {
print("Audio session error: \(error)")
}
}

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 !AppServices.corePreferences.disableAddContact && !AppServices.corePreferences.hideContactEdition {
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

@ -29,8 +29,10 @@ struct ContactFragment: View {
@Binding var isShowDeletePopup: Bool
@Binding var isShowDismissPopup: Bool
@Binding var isShowTrustLevelPopup: Bool
@Binding var isShowSipAddressesPopup: Bool
@Binding var isShowSipAddressesPopupType: Int
@Binding var isShowIncreaseTrustLevelPopup: Bool
@Binding var isShowEditContactFragmentInContactDetails: Bool
@State private var showingSheet = false
@ -68,19 +70,11 @@ struct ContactFragment: View {
showingSheet: $showingSheet,
showShareSheet: $showShareSheet,
isShowDismissPopup: $isShowDismissPopup,
isShowTrustLevelPopup: $isShowTrustLevelPopup,
isShowSipAddressesPopup: $isShowSipAddressesPopup,
isShowSipAddressesPopupType: $isShowSipAddressesPopupType,
isShowIncreaseTrustLevelPopup: $isShowIncreaseTrustLevelPopup,
isShowEditContactFragmentInContactDetails: $isShowEditContactFragmentInContactDetails
)
}
}
#Preview {
ContactFragment(
isShowDeletePopup: .constant(false),
isShowDismissPopup: .constant(false),
isShowSipAddressesPopup: .constant(false),
isShowSipAddressesPopupType: .constant(0),
isShowEditContactFragmentInContactDetails: .constant(false)
)
}

View file

@ -23,143 +23,178 @@ import linphonesw
// swiftlint:disable type_body_length
struct ContactInnerActionsFragment: View {
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject private var contactsManager = ContactsManager.shared
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@EnvironmentObject var contactAvatarModel: ContactAvatarModel
@EnvironmentObject var contactsListViewModel: ContactsListViewModel
@State private var trustIsOpen = true
@State private var informationIsOpen = true
@Binding var showingSheet: Bool
@Binding var showShareSheet: Bool
@Binding var isShowDeletePopup: Bool
@Binding var isShowDismissPopup: Bool
@Binding var isShowTrustLevelPopup: Bool
@Binding var isShowMediaFilesFragment: Bool
@Binding var isShowDocumentsFilesFragment: Bool
@Binding var isShowIncreaseTrustLevelPopup: Bool
@Binding var isShowEditContactFragmentInContactDetails: Bool
let geometry: GeometryProxy
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()
var body: some View {
if !AppServices.corePreferences.hideSipAddresses || (AppServices.corePreferences.hideSipAddresses && !contactAvatarModel.phoneNumbersWithLabel.isEmpty) {
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) {
if !AppServices.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)
}
.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)
}
}
}
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)
}
}
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
.background(Color.gray100)
}
} else {
HStack {}
.frame(height: 20)
}
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)
}
}
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()
}
.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()
}
.padding(.horizontal)
}
}
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
if !contactAvatarModel.organization.isEmpty || !contactAvatarModel.jobTitle.isEmpty {
VStack {
@ -188,34 +223,207 @@ struct ContactInnerActionsFragment: View {
.transition(.move(edge: .top))
}
// TODO Trust Fragment
// TODO Medias Fragment
HStack(alignment: .center) {
Text("contact_details_actions_title")
.default_text_style_800(styleSize: 16)
Button {
isShowTrustLevelPopup = true
} label: {
HStack {
Text("contact_details_trust_title")
.default_text_style_800(styleSize: 15)
Image("question")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 22, height: 22)
}
}
Spacer()
Image(trustIsOpen ? "caret-up" : "caret-down")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
.background(Color.gray100)
.onTapGesture {
withAnimation {
trustIsOpen.toggle()
}
}
VStack(spacing: 0) {
if !contactAvatarModel.nativeUri.isEmpty {
if trustIsOpen {
VStack(spacing: 0) {
if !contactsListViewModel.devices.isEmpty {
Text("contact_details_trusted_devices_count")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 20)
.padding(.horizontal, 20)
.padding(.bottom, 10)
let radius = geometry.size.height * 0.5
let barWidth = min(geometry.size.width - 70, SharedMainViewModel.shared.maxWidth - 70)
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(Color.blueInfo500.opacity(0.2))
.frame(width: barWidth, height: 30)
.clipShape(RoundedRectangle(cornerRadius: radius))
if contactsListViewModel.trustedDevicesPercentage >= 15 {
Rectangle()
.foregroundColor(Color.blueInfo500)
.frame(width: ((contactsListViewModel.trustedDevicesPercentage / 100) * barWidth) - 6, height: 25)
.clipShape(RoundedRectangle(cornerRadius: radius))
.padding(.horizontal, 3)
} else if contactsListViewModel.trustedDevicesPercentage > 0 {
Rectangle()
.foregroundColor(Color.blueInfo500)
.frame(width: ((10 / 100) * barWidth) - 6, height: 25)
.clipShape(RoundedRectangle(cornerRadius: radius))
.padding(.horizontal, 3)
}
if contactsListViewModel.trustedDevicesPercentage >= 30 {
Text(String(Int(contactsListViewModel.trustedDevicesPercentage)) + "%")
.default_text_style_white_700(styleSize: 14)
.frame(width: (contactsListViewModel.trustedDevicesPercentage / 100) * barWidth, height: 25, alignment: .center)
} else {
Text(String(Int(contactsListViewModel.trustedDevicesPercentage)) + "%")
.foregroundStyle(contactsListViewModel.trustedDevicesPercentage == 0 ? Color.redDanger500 : Color.blueInfo500)
.default_text_style_white_700(styleSize: 14)
.frame(width: barWidth, height: 25, alignment: .center)
}
}
.frame(width: barWidth, height: 30)
.contentShape(Rectangle())
.padding(.bottom, 10)
ForEach(contactsListViewModel.devices) { device in
HStack {
Text(device.name)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
HStack {
if !device.trusted {
Button {
SharedMainViewModel.shared.increaseTrustLevelPopupDeviceName = device.name
SharedMainViewModel.shared.increaseTrustLevelPopupDeviceAddress = device.address
isShowIncreaseTrustLevelPopup = true
} label: {
HStack {
Image("warning-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25)
.padding(.all, 6)
Text("contact_make_call_check_device_trust")
.foregroundStyle(Color.orangeMain500)
.default_text_style(styleSize: 14)
.lineLimit(1)
.padding(.leading, -5)
.padding(.trailing, 15)
}
}
.background(Color.orangeMain100)
.cornerRadius(25)
} else {
ZStack {
Button {
} label: {
HStack {
Image("warning-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25)
.padding(.all, 6)
Text("contact_make_call_check_device_trust")
.foregroundStyle(Color.orangeMain500)
.default_text_style(styleSize: 14)
.lineLimit(1)
.padding(.leading, -5)
.padding(.trailing, 15)
}
}
.background(Color.orangeMain100)
.cornerRadius(25)
.hidden()
Image("trusted")
.resizable()
.frame(width: 28, height: 28)
}
}
}
.frame(height: 40)
}
.background(.white)
.padding(.vertical, 10)
.padding(.horizontal, 20)
}
} else {
Text("contact_details_no_device_found")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 15)
.padding(.horizontal, 20)
.padding(.bottom, 10)
}
}
.padding(.bottom, 5)
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-2)
.transition(.move(edge: .top))
}
if sharedMainViewModel.displayedFriendExistingChatRoom != nil {
HStack(alignment: .center) {
Text("conversation_details_media_documents_title")
.default_text_style_800(styleSize: 16)
Spacer()
Image("caret-up")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
.hidden()
}
.padding(.top, 20)
.padding(.bottom, 10)
.padding(.horizontal, 16)
.background(Color.gray100)
VStack(spacing: 0) {
Button {
actionEditButton()
withAnimation {
isShowMediaFilesFragment = true
}
} label: {
HStack {
Image("pencil-simple")
Image("image")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text("contact_details_edit")
Text("conversation_menu_media_files")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
@ -225,11 +433,68 @@ struct ContactInnerActionsFragment: View {
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
} else {
NavigationLink(destination: EditContactFragment(
contactAvatarModel: contactAvatarModel,
isShowEditContactFragment: $isShowEditContactFragmentInContactDetails,
isShowDismissPopup: $isShowDismissPopup)) {
VStack {
Divider()
}
.padding(.horizontal)
Button {
withAnimation {
isShowDocumentsFilesFragment = true
}
} label: {
HStack {
Image("file-pdf")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
Text("conversation_menu_documents_files")
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
}
.background(.white)
.cornerRadius(15)
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
HStack(alignment: .center) {
Text("contact_details_actions_title")
.default_text_style_800(styleSize: 16)
Spacer()
Image("caret-up")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
.hidden()
}
.padding(.top, 20)
.padding(.bottom, 10)
.padding(.horizontal, 16)
.background(Color.gray100)
VStack(spacing: 0) {
if !contactAvatarModel.isReadOnly && !AppServices.corePreferences.hideContactEdition {
if !contactAvatarModel.editable {
Button {
actionEditButton()
} label: {
HStack {
Image("pencil-simple")
.renderingMode(.template)
@ -237,7 +502,7 @@ struct ContactInnerActionsFragment: View {
.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)
@ -247,47 +512,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 +599,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 && !AppServices.corePreferences.hideContactEdition {
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)
@ -343,18 +634,6 @@ struct ContactInnerActionsFragment: View {
.padding(.horizontal)
.zIndex(-1)
.transition(.move(edge: .top))
}
}
}
#Preview {
ContactInnerActionsFragment(
showingSheet: .constant(false),
showShareSheet: .constant(false),
isShowDeletePopup: .constant(false),
isShowDismissPopup: .constant(false),
isShowEditContactFragmentInContactDetails: .constant(false),
actionEditButton: {}
)
}
// swiftlint:enable type_body_length

View file

@ -34,244 +34,311 @@ struct ContactInnerFragment: View {
@State var cnContact: CNContact?
@State private var presentingEditContact = false
@State private var isShowMediaFilesFragment = false
@State private var isShowDocumentsFilesFragment = false
@Binding var isShowDeletePopup: Bool
@Binding var showingSheet: Bool
@Binding var showShareSheet: Bool
@Binding var isShowDismissPopup: Bool
@Binding var isShowTrustLevelPopup: Bool
@Binding var isShowSipAddressesPopup: Bool
@Binding var isShowSipAddressesPopupType: Int
@Binding var isShowIncreaseTrustLevelPopup: Bool
@Binding var isShowEditContactFragmentInContactDetails: Bool
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 1) {
Rectangle()
.foregroundColor(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
HStack {
if !(orientation == .landscapeLeft || orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Image("caret-left")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
.padding(.top, 2)
.padding(.leading, -10)
.onTapGesture {
withAnimation {
SharedMainViewModel.shared.displayedFriend = nil
}
}
}
GeometryReader { geometry in
ZStack {
VStack(spacing: 1) {
Rectangle()
.foregroundColor(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
Spacer()
if !contactAvatarModel.nativeUri.isEmpty {
Button(action: {
editNativeContact()
}, label: {
Image("pencil-simple")
HStack {
if !(orientation == .landscapeLeft || orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Image("caret-left")
.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)) {
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
.padding(.leading, -10)
.onTapGesture {
withAnimation {
SharedMainViewModel.shared.displayedFriend = nil
}
}
)
}
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.padding(.horizontal)
.padding(.bottom, 4)
.background(.white)
ScrollView {
VStack(spacing: 0) {
VStack(spacing: 0) {
VStack(spacing: 0) {
if SharedMainViewModel.shared.displayedFriend != nil {
Avatar(contactAvatarModel: contactAvatarModel, avatarSize: 100)
Text(contactAvatarModel.name)
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity)
.padding(.top, 10)
Text(contactAvatarModel.lastPresenceInfo)
.foregroundStyle(contactAvatarModel.lastPresenceInfo == "Online"
? Color.greenSuccess500
: Color.orangeWarning600)
.multilineTextAlignment(.center)
.default_text_style_300(styleSize: 12)
.frame(maxWidth: .infinity)
}
}
.frame(minHeight: 150)
.frame(maxWidth: .infinity)
.padding(.top, 10)
.background(Color.gray100)
HStack {
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) ")
}
} else {
isShowSipAddressesPopupType = 0
isShowSipAddressesPopup = true
}
}, label: {
VStack {
HStack(alignment: .center) {
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(contactAvatarModel.address.isEmpty ? Color.grayMain2c400 : Color.grayMain2c600)
.frame(width: 25, height: 25)
}
.padding(16)
.background(contactAvatarModel.address.isEmpty ? Color.grayMain2c100 : 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
}
}, label: {
VStack {
HStack(alignment: .center) {
Image("chat-teardrop-text")
.renderingMode(.template)
.resizable()
.foregroundStyle(contactAvatarModel.address.isEmpty ? Color.grayMain2c400 : Color.grayMain2c600)
.frame(width: 25, height: 25)
}
.padding(16)
.background(contactAvatarModel.address.isEmpty ? Color.grayMain2c100 : 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) ")
}
} 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)
}
.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()
}
.padding(.top, 20)
.frame(maxWidth: .infinity)
.background(Color.gray100)
ContactInnerActionsFragment(
showingSheet: $showingSheet,
showShareSheet: $showShareSheet,
isShowDeletePopup: $isShowDeletePopup,
isShowDismissPopup: $isShowDismissPopup,
isShowEditContactFragmentInContactDetails: $isShowEditContactFragmentInContactDetails,
actionEditButton: editNativeContact
)
}
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
Spacer()
if !contactAvatarModel.isReadOnly && !AppServices.corePreferences.hideContactEdition {
if !contactAvatarModel.editable {
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)) {
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)
.frame(height: 50)
.padding(.horizontal)
.padding(.bottom, 4)
.background(.white)
ScrollView {
VStack(spacing: 0) {
VStack(spacing: 0) {
VStack(spacing: 0) {
if SharedMainViewModel.shared.displayedFriend != nil {
Avatar(contactAvatarModel: contactAvatarModel, avatarSize: 100)
Text(contactAvatarModel.name)
.foregroundStyle(Color.grayMain2c700)
.multilineTextAlignment(.center)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity)
.padding(.top, 10)
Text(contactAvatarModel.lastPresenceInfo)
.foregroundStyle(contactAvatarModel.lastPresenceInfo == "Online"
? Color.greenSuccess500
: Color.orangeWarning600)
.multilineTextAlignment(.center)
.default_text_style_300(styleSize: 12)
.frame(maxWidth: .infinity)
}
}
.frame(minHeight: 150)
.frame(maxWidth: .infinity)
.padding(.top, 10)
.background(Color.gray100)
HStack {
Spacer()
Button(action: {
CoreContext.shared.doOnCoreQueue { core in
if contactAvatarModel.addresses.count == 1 && contactAvatarModel.phoneNumbersWithLabel.isEmpty {
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.isEmpty && 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
}
}
}
}, label: {
VStack {
HStack(alignment: .center) {
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
}
.padding(16)
.background(Color.grayMain2c200)
.cornerRadius(40)
Text("contact_call_action")
.default_text_style(styleSize: 14)
}
})
if !AppServices.corePreferences.disableChatFeature {
Spacer()
Button(action: {
CoreContext.shared.doOnCoreQueue { core in
if contactAvatarModel.addresses.count == 1 && contactAvatarModel.phoneNumbersWithLabel.isEmpty {
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.isEmpty && 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(Color.grayMain2c600)
.frame(width: 25, height: 25)
}
.padding(16)
.background(Color.grayMain2c200)
.cornerRadius(40)
Text("contact_message_action")
.default_text_style(styleSize: 14)
}
})
}
Spacer()
if !SharedMainViewModel.shared.disableVideoCall {
Button(action: {
CoreContext.shared.doOnCoreQueue { core in
if contactAvatarModel.addresses.count == 1 && contactAvatarModel.phoneNumbersWithLabel.isEmpty {
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.isEmpty && 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
}
}
}
}, 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)
}
})
Spacer()
}
}
.padding(.top, 20)
.frame(maxWidth: .infinity)
.background(Color.gray100)
ContactInnerActionsFragment(
showingSheet: $showingSheet,
showShareSheet: $showShareSheet,
isShowDeletePopup: $isShowDeletePopup,
isShowDismissPopup: $isShowDismissPopup,
isShowTrustLevelPopup: $isShowTrustLevelPopup,
isShowMediaFilesFragment: $isShowMediaFilesFragment,
isShowDocumentsFilesFragment: $isShowDocumentsFilesFragment,
isShowIncreaseTrustLevelPopup: $isShowIncreaseTrustLevelPopup,
isShowEditContactFragmentInContactDetails: $isShowEditContactFragmentInContactDetails,
geometry: geometry,
actionEditButton: editNativeContact
)
.onAppear {
contactsListViewModel.fetchDevicesAndTrust()
contactsListViewModel.getOneToOneChatRoomWith()
}
.onChange(of: SharedMainViewModel.shared.displayedFriend?.id) { _ in
isShowMediaFilesFragment = false
isShowDocumentsFilesFragment = false
SharedMainViewModel.shared.displayedFriendExistingChatRoom = nil
contactsListViewModel.fetchDevicesAndTrust()
contactsListViewModel.getOneToOneChatRoomWith()
}
.onDisappear {
SharedMainViewModel.shared.displayedFriendExistingChatRoom = nil
}
}
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
}
.frame(maxWidth: .infinity)
}
.background(Color.gray100)
}
.background(Color.gray100)
}
.background(.white)
.navigationBarHidden(true)
.onRotate { newOrientation in
orientation = newOrientation
}
.fullScreenCover(isPresented: $presentingEditContact) {
NavigationView {
EditContactView(contact: $cnContact)
.navigationBarTitle("contact_edit_title")
.navigationBarTitleDisplayMode(.inline)
.edgesIgnoringSafeArea(.vertical)
.background(.white)
.navigationBarHidden(true)
.onRotate { newOrientation in
orientation = newOrientation
}
.fullScreenCover(isPresented: $presentingEditContact) {
NavigationView {
EditContactView(contact: $cnContact)
.navigationBarTitle("contact_edit_title")
.navigationBarTitleDisplayMode(.inline)
.edgesIgnoringSafeArea(.vertical)
}
}
if isShowMediaFilesFragment {
ConversationMediaListFragment(
isShowMediaFilesFragment: $isShowMediaFilesFragment
)
.zIndex(5)
.transition(.move(edge: .trailing))
}
if isShowDocumentsFilesFragment {
ConversationDocumentsListFragment(
isShowDocumentsFilesFragment: $isShowDocumentsFilesFragment
)
.zIndex(5)
.transition(.move(edge: .trailing))
}
}
}
@ -296,15 +363,3 @@ struct ContactInnerFragment: View {
}
}
}
#Preview {
ContactInnerFragment(
isShowDeletePopup: .constant(false),
showingSheet: .constant(false),
showShareSheet: .constant(false),
isShowDismissPopup: .constant(false),
isShowSipAddressesPopup: .constant(false),
isShowSipAddressesPopupType: .constant(0),
isShowEditContactFragmentInContactDetails: .constant(false)
)
}

View file

@ -52,7 +52,6 @@ struct ContactListBottomSheet: View {
.padding(.trailing)
}
Spacer()
Button {
UIPasteboard.general.setValue(
contactsListViewModel.stringToCopy.prefix(4) == "sip:"
@ -67,8 +66,7 @@ struct ContactListBottomSheet: View {
dismiss()
}
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
ToastViewModel.shared.displayToast.toggle()
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
} label: {
HStack {

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 && !AppServices.corePreferences.hideContactEdition {
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 && !AppServices.corePreferences.hideContactEdition {
VStack {
Divider()
}
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
.frame(maxWidth: .infinity)
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

@ -99,10 +99,17 @@ struct ContactRow: View {
SharedMainViewModel.shared.displayedFriend = contactAvatarModel
}
}
if contactAvatarModel.friend != nil
&& contactAvatarModel.friend!.address != nil {
startCallFunc(contactAvatarModel.friend!.address!)
CoreContext.shared.doOnCoreQueue { core in
if let friend = contactAvatarModel.friend {
if let friendAddress = friend.address {
startCallFunc(friendAddress)
} else if !friend.phoneNumbers.isEmpty {
if let address = core.interpretUrl(url: friend.phoneNumbers.first ?? "", applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) {
startCallFunc(address)
}
}
}
}
}
.onLongPressGesture(minimumDuration: 0.2) {

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: AppServices.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

@ -85,7 +85,51 @@ struct SipAddressesPopup: View {
}
}
} catch {
Log.error("[ContactInnerActionsFragment] unable to create address for a new outgoing call : \(contactAvatarModel.addresses[index]) \(error) ")
Log.error("[SipAddressesPopup] unable to create address for a new outgoing call : \(contactAvatarModel.addresses[index]) \(error) ")
}
}
}
ForEach(0..<contactAvatarModel.phoneNumbersWithLabel.count, id: \.self) { index in
HStack {
HStack {
VStack {
Text(String(localized: "phone_number") + ":")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Text(contactAvatarModel.phoneNumbersWithLabel[index].phoneNumber)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 10)
}
.background(.white)
.onTapGesture {
CoreContext.shared.doOnCoreQueue { core in
if let address = core.interpretUrl(url: contactAvatarModel.phoneNumbersWithLabel[index].phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) {
DispatchQueue.main.async {
if isShowSipAddressesPopupType != 1 {
withAnimation {
isShowSipAddressesPopup = false
telecomManager.doCallOrJoinConf(address: address, isVideo: isShowSipAddressesPopupType == 2)
isShowSipAddressesPopupType = 0
}
} else {
withAnimation {
isShowSipAddressesPopup = false
contactsListViewModel.createOneToOneChatRoomWith(remote: address)
isShowSipAddressesPopupType = 0
}
}
}
} else {
Log.error("[SipAddressesPopup] unable to create address (interpret Url for phone number) for a new outgoing call : \(contactAvatarModel.addresses[index])")
}
}
}
}
@ -97,6 +141,7 @@ struct SipAddressesPopup: View {
.frame(maxHeight: .infinity)
.shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2)
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
.padding(.horizontal, 20)
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
}
}

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
@ -43,6 +45,8 @@ class ContactAvatarModel: ObservableObject, Identifiable {
@Published var photo: String = ""
@Published var lastPresenceInfo: String = ""
@Published var presenceStatus: ConsolidatedPresence = .Offline
@Published var unsafeFriend: Bool = false
@Published var trustedFriend: Bool = false
private var friendDelegate: FriendDelegate?
@ -70,15 +74,26 @@ 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
let unsafeFriendTmp = (friend?.securityLevel ?? .None) == .Unsafe
let trustedFriendTmp = (friend?.securityLevel ?? .None) == .EndToEndEncryptedAndVerified
if let friend = friend, withPresence == true {
lastPresenceInfoTmp = ""
@ -108,6 +123,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
@ -116,6 +133,8 @@ class ContactAvatarModel: ObservableObject, Identifiable {
self.photo = photoTmp
self.lastPresenceInfo = lastPresenceInfoTmp
self.presenceStatus = presenceStatusTmp
self.unsafeFriend = unsafeFriendTmp
self.trustedFriend = trustedFriendTmp
}
}
}
@ -192,6 +211,17 @@ class ContactAvatarModel: ObservableObject, Identifiable {
if avatarModel == nil {
avatarModel = ContactAvatarModel(friend: nil, name: addressFriend.name!, address: addressFriend.address!.asStringUriOnly(), withPresence: false)
}
completion(avatarModel!)
} else if !addressFriend.phoneNumbers.isEmpty {
var avatarModel = ContactsManager.shared.avatarListModel.first(where: {
$0.friend != nil && $0.friend!.name == addressFriend.name && !$0.friend!.phoneNumbers.isEmpty
&& $0.friend!.phoneNumbers == addressFriend.phoneNumbers
})
if avatarModel == nil {
avatarModel = ContactAvatarModel(friend: nil, name: addressFriend.name!, address: addressFriend.phoneNumbers.first ?? addressFriend.address?.asStringUriOnly() ?? "", withPresence: false)
}
completion(avatarModel!)
} else {
var name = ""

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of Linphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Foundation
import linphonesw
class ContactDeviceModel: ObservableObject, Identifiable {
let id = UUID()
var name: String
var address: Address
var trusted: Bool
init(name: String, address: Address, trusted: Bool) {
self.name = name
self.address = address
self.trusted = trusted
}
}

View file

@ -23,6 +23,8 @@ import SwiftUI
// swiftlint:disable line_length
class ContactsListViewModel: ObservableObject {
static let TAG = "[ConversationForwardMessageViewModel]"
@Published var selectedEditFriend: ContactAvatarModel?
var stringToCopy: String = ""
@ -31,18 +33,28 @@ class ContactsListViewModel: ObservableObject {
var selectedFriendToShare: ContactAvatarModel?
var selectedFriendToDelete: ContactAvatarModel?
@Published var devices: [ContactDeviceModel] = []
@Published var trustedDevicesPercentage: Double = 0.0
@Published var displayedConversation: ConversationModel?
private var coreDelegate: CoreDelegate?
private var contactChatRoomDelegate: ChatRoomDelegate?
init() {}
private let nativeAddressBookFriendList = "Native address-book"
let linphoneAddressBookFriendList = "Linphone address-book"
let tempRemoteAddressBookFriendList = "TempRemoteDirectoryContacts address-book"
init() {
addCoreDelegate()
}
func createOneToOneChatRoomWith(remote: Address) {
CoreContext.shared.doOnCoreQueue { core in
let account = core.defaultAccount
if account == nil {
Log.error(
"\(ConversationForwardMessageViewModel.TAG) No default account found, can't create conversation with \(remote.asStringUriOnly())"
"\(Self.TAG) No default account found, can't create conversation with \(remote.asStringUriOnly())"
)
return
}
@ -61,7 +73,7 @@ class ContactsListViewModel: ObservableObject {
guard let chatParams = params.chatParams else { return }
chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
let sameDomain = remote.domain == CorePreferences.defaultDomain && remote.domain == account!.params?.domain
let sameDomain = remote.domain == AppServices.corePreferences.defaultDomain && remote.domain == account!.params?.domain
if account!.params != nil && (account!.params!.instantMessagingEncryptionMandatory && sameDomain) {
Log.info("\(ConversationForwardMessageViewModel.TAG) Account is in secure mode & domain matches, creating an E2E encrypted conversation")
chatParams.backend = ChatRoom.Backend.FlexisipChat
@ -87,8 +99,7 @@ class ContactsListViewModel: ObservableObject {
DispatchQueue.main.async {
SharedMainViewModel.shared.operationInProgress = false
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
return
}
@ -133,8 +144,7 @@ class ContactsListViewModel: ObservableObject {
DispatchQueue.main.async {
SharedMainViewModel.shared.operationInProgress = false
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
}
} else {
@ -164,8 +174,7 @@ class ContactsListViewModel: ObservableObject {
}
DispatchQueue.main.async {
SharedMainViewModel.shared.operationInProgress = false
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
}
}, onConferenceJoined: { (chatRoom: ChatRoom, _: EventLog) in
@ -203,14 +212,37 @@ class ContactsListViewModel: ObservableObject {
}
DispatchQueue.main.async {
SharedMainViewModel.shared.operationInProgress = false
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
ToastViewModel.shared.displayToast = true
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
}
})
chatRoom.addDelegate(delegate: contactChatRoomDelegate!)
}
func addCoreDelegate() {
CoreContext.shared.doOnCoreQueue { core in
if let coreDelegate = self.coreDelegate {
core.removeDelegate(delegate: coreDelegate)
self.coreDelegate = nil
}
self.coreDelegate = CoreDelegateStub(
onCallStateChanged: { (core: Core, call: Call, state: Call.State, message: String) in
if call.state == Call.State.End && SharedMainViewModel.shared.displayedFriend != nil {
// Updates trust if need be
DispatchQueue.main.async {
self.fetchDevicesAndTrust()
}
}
}
)
if self.coreDelegate != nil {
core.addDelegate(delegate: self.coreDelegate!)
}
}
}
func deleteSelectedContact() {
CoreContext.shared.doOnCoreQueue { core in
if self.selectedFriendToDelete != nil && self.selectedFriendToDelete!.friend != nil {
@ -266,5 +298,141 @@ class ContactsListViewModel: ObservableObject {
}
}
}
func fetchDevicesAndTrust() {
if let friend = SharedMainViewModel.shared.displayedFriend?.friend {
var devicesList: [ContactDeviceModel] = []
let friendDevices = friend.devices
if friendDevices.isEmpty {
Log.info("\(Self.TAG) No device found for friend [\(friend.name ?? "")]")
} else {
let devicesCount = friendDevices.count
var trustedDevicesCount = 0
for device in friendDevices {
let trusted = device.securityLevel == .EndToEndEncryptedAndVerified
if let address = device.address {
devicesList.append(
ContactDeviceModel(
name: device.displayName ?? NSLocalizedString("contact_device_without_name", comment: ""),
address: address,
trusted: trusted
)
)
}
if trusted {
trustedDevicesCount += 1
}
}
if !devicesList.isEmpty {
let percentage = trustedDevicesCount * 100 / devicesCount
trustedDevicesPercentage = Double(percentage)
}
}
devices = devicesList
}
}
func getOneToOneChatRoomWith() {
CoreContext.shared.doOnCoreQueue { core in
if let contactAvatarModel = SharedMainViewModel.shared.displayedFriend {
var remote: Address?
if contactAvatarModel.addresses.count >= 1 {
do {
remote = try Factory.Instance.createAddress(addr: contactAvatarModel.address)
} catch {
Log.error("\(Self.TAG) unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error)")
return
}
} else if contactAvatarModel.addresses.isEmpty &&
contactAvatarModel.phoneNumbersWithLabel.count == 1 {
if let firstPhone = contactAvatarModel.phoneNumbersWithLabel.first,
let address = core.interpretUrl(
url: firstPhone.phoneNumber,
applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)
) {
remote = address
}
}
guard let remote else {
Log.error("\(Self.TAG) No valid remote address found")
return
}
let account = core.defaultAccount
if account == nil {
Log.error(
"\(Self.TAG) No default account found, can't create conversation with \(remote.asStringUriOnly())"
)
return
}
do {
let params = try core.createConferenceParams(conference: nil)
params.chatEnabled = true
params.groupEnabled = false
params.subject = NSLocalizedString("conversation_one_to_one_hidden_subject", comment: "")
params.account = account
guard let chatParams = params.chatParams else { return }
chatParams.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
let sameDomain = remote.domain == AppServices.corePreferences.defaultDomain && remote.domain == account!.params?.domain
if account!.params != nil && (account!.params!.instantMessagingEncryptionMandatory && sameDomain) {
Log.info("\(ConversationForwardMessageViewModel.TAG) Account is in secure mode & domain matches, creating an E2E encrypted conversation")
chatParams.backend = ChatRoom.Backend.FlexisipChat
params.securityLevel = Conference.SecurityLevel.EndToEnd
} else if account!.params != nil && (!account!.params!.instantMessagingEncryptionMandatory) {
if LinphoneUtils.isEndToEndEncryptedChatAvailable(core: core) {
Log.info(
"\(ConversationForwardMessageViewModel.TAG) Account is in interop mode but LIME is available, creating an E2E encrypted conversation"
)
chatParams.backend = ChatRoom.Backend.FlexisipChat
params.securityLevel = Conference.SecurityLevel.EndToEnd
} else {
Log.info(
"\(ConversationForwardMessageViewModel.TAG) Account is in interop mode but LIME isn't available, creating a SIP simple conversation"
)
chatParams.backend = ChatRoom.Backend.Basic
params.securityLevel = Conference.SecurityLevel.None
}
} else {
Log.error(
"\(ConversationForwardMessageViewModel.TAG) Account is in secure mode, can't chat with SIP address of different domain \(remote.asStringUriOnly())"
)
DispatchQueue.main.async {
SharedMainViewModel.shared.operationInProgress = false
ToastViewModel.shared.show("Failed_to_create_conversation_error")
}
return
}
let participants = [remote]
let localAddress = account?.params?.identityAddress
if let existingChatRoomTmp = core.searchChatRoom(params: params, localAddr: localAddress, remoteAddr: nil, participants: participants) {
Log.warn(
"\(ConversationForwardMessageViewModel.TAG) A 1-1 conversation between local account \(localAddress?.asStringUriOnly() ?? "") and remote \(remote.asStringUriOnly()) for given parameters already exists!"
)
let conversationModel = ConversationModel(chatRoom: existingChatRoomTmp)
DispatchQueue.main.async {
SharedMainViewModel.shared.displayedFriendExistingChatRoom = conversationModel
}
}
} catch {
}
}
}
}
}
// swiftlint:enable line_length

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] = []

File diff suppressed because it is too large Load diff

View file

@ -55,7 +55,7 @@ struct ConversationsView: View {
#Preview {
ConversationsListFragment(
showingSheet: .constant(false),
text: .constant("")
text: .constant(""),
showingSheet: .constant(false)
)
}

View file

@ -20,6 +20,7 @@
import SwiftUI
import WebKit
import QuickLook
import Combine
// swiftlint:disable type_body_length
// swiftlint:disable cyclomatic_complexity
@ -52,7 +53,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 +138,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)
@ -145,7 +152,8 @@ struct ChatBubbleView: View {
.clipShape(RoundedRectangle(cornerRadius: 1))
.roundedCorner(
16,
corners: eventLogMessage.message.isOutgoing ? [.topLeft, .topRight, .bottomLeft] : [.topLeft, .topRight, .bottomRight]
corners: eventLogMessage.message.isOutgoing ? [.topLeft, .topRight, .bottomLeft] : [.topLeft, .topRight, .bottomRight],
stroke: eventLogMessage.message.id == conversationViewModel.highlightedMessageID
)
}
.onTapGesture {
@ -173,7 +181,18 @@ struct ChatBubbleView: View {
}
if !eventLogMessage.message.text.isEmpty {
DynamicLinkText(text: eventLogMessage.message.text)
DynamicLinkText(
text: eventLogMessage.message.text,
isMessageId: eventLogMessage.message.id == conversationViewModel.highlightedMessageID,
searchText: conversationViewModel.searchText,
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 +344,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 +376,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)
@ -387,7 +422,9 @@ struct ChatBubbleView: View {
.roundedCorner(
16,
corners: eventLogMessage.message.isOutgoing && eventLogMessage.message.isFirstMessage ? [.topLeft, .topRight, .bottomLeft] :
(!eventLogMessage.message.isOutgoing && eventLogMessage.message.isFirstMessage ? [.topRight, .bottomRight, .bottomLeft] : [.allCorners]))
(!eventLogMessage.message.isOutgoing && eventLogMessage.message.isFirstMessage ? [.topRight, .bottomRight, .bottomLeft] : [.allCorners]),
stroke: eventLogMessage.message.id == conversationViewModel.highlightedMessageID
)
if !eventLogMessage.message.reactions.isEmpty {
HStack {
@ -849,7 +886,7 @@ struct ChatBubbleView: View {
}
}
}
.frame(width: geometryProxy.size.width - 150)
.frame(width: max(0, geometryProxy.size.width - 150))
}
}
@ -918,40 +955,123 @@ struct ChatBubbleView: View {
struct DynamicLinkText: View {
let text: String
let isMessageId: Bool
let searchText: 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)
// MARK: - Builder
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)
highlightSearch(in: &result, originalText: text)
return result
}
// MARK: - Word handling
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", "sip", "sips"].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)
result.append(mention)
return
}
// Text
var normal = AttributedString(word)
normal.foregroundColor = Color.grayMain2c700
result.append(normal)
}
// MARK: - Highlight global
private func highlightSearch(
in attributed: inout AttributedString,
originalText: String
) {
guard !searchText.isEmpty && isMessageId else { return }
let base = originalText.folding(
options: [.caseInsensitive, .diacriticInsensitive],
locale: .current
)
let search = searchText.folding(
options: [.caseInsensitive, .diacriticInsensitive],
locale: .current
)
var searchRange = base.startIndex..<base.endIndex
while let found = base.range(of: search, range: searchRange) {
guard
let start = AttributedString.Index(found.lowerBound, within: attributed),
let end = AttributedString.Index(found.upperBound, within: attributed)
else { break }
attributed[start..<end].font = .system(size: 14, weight: .bold)
searchRange = found.upperBound..<base.endIndex
}
}
// MARK: - Mention validation
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 {
@ -1029,8 +1149,12 @@ struct RoundedCorner: Shape {
}
extension View {
func roundedCorner(_ radius: CGFloat, corners: UIRectCorner) -> some View {
func roundedCorner(_ radius: CGFloat, corners: UIRectCorner, stroke: Bool? = false) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners) )
.overlay(
RoundedCorner(radius: radius, corners: corners)
.stroke(Color.orangeMain500, lineWidth: (stroke ?? false) ? 1 : 0)
)
}
}
@ -1039,12 +1163,14 @@ struct CustomSlider: View {
let eventLogMessage: EventLogMessage
@State private var timer: Timer?
@State private var value: Double = 0.0
@State private var isPlaying: Bool = false
@State private var timer: Timer?
@State private var cancellable: AnyCancellable?
var minTrackColor: Color = .white.opacity(0.5)
var maxTrackGradient: Gradient = Gradient(colors: [Color.orangeMain300, Color.orangeMain500])
var maxTrackGradient: Gradient = Gradient(colors: [Color.orangeMain500.opacity(0.5), Color.orangeMain500])
var body: some View {
GeometryReader { geometry in
@ -1100,7 +1226,26 @@ struct CustomSlider: View {
.padding(.horizontal, 10)
}
.clipShape(RoundedRectangle(cornerRadius: radius))
.onAppear {
if eventLogMessage.message.attachments.first?.type == .voiceRecording {
cancellable =
NotificationCenter.default
.publisher(for: NSNotification.Name("VoiceRecording"))
.compactMap { $0.userInfo?["messageId"] as? String }
.sink { messageId in
if messageId == eventLogMessage.message.id {
conversationViewModel.startVoiceRecordPlayer(
voiceRecordPath: eventLogMessage.message.attachments.first!.full
)
playProgress()
}
}
}
}
.onDisappear {
cancellable?.cancel()
cancellable = nil
resetProgress()
}
}
@ -1124,7 +1269,23 @@ struct CustomSlider: View {
}
}
} else {
resetProgress()
self.resetProgress()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
let rows = conversationViewModel.conversationMessagesSection[0].rows
if let index = rows.firstIndex(where: { $0.eventModel.eventLogId == eventLogMessage.message.id }),
rows.indices.contains(index - 1) {
let nextRow = rows[index - 1]
if nextRow.message.attachments.first?.type == .voiceRecording {
NotificationCenter.default.post(
name: NSNotification.Name("VoiceRecording"),
object: nil,
userInfo: ["messageId": nextRow.message.id]
)
}
}
}
}
}
}

View file

@ -0,0 +1,137 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SwiftUI
import linphonesw
struct ConversationDeleteMessageBottomSheet: View {
@Environment(\.dismiss) var dismiss
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@State private var orientation = UIDevice.current.orientation
@EnvironmentObject var conversationViewModel: ConversationViewModel
@Binding var showingSheet: Bool
var body: some View {
VStack(alignment: .leading) {
if idiom != .pad && (orientation == .landscapeLeft
|| orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Spacer()
HStack {
Spacer()
Button("dialog_close") {
if #available(iOS 16.0, *) {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
}
}
.padding(.trailing)
}
Spacer()
Button {
NotificationCenter.default.post(name: NSNotification.Name("DeleteMessageForEveryone"), object: nil)
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} else {
showingSheet.toggle()
dismiss()
}
} else {
showingSheet.toggle()
dismiss()
}
} label: {
HStack {
Image("trash-simple")
.renderingMode(.template)
.resizable()
.foregroundStyle(conversationViewModel.selectedMessage?.message.isRetractable == true ? Color.redDanger500 : Color.gray200)
.frame(width: 25, height: 25, alignment: .leading)
Text("conversation_dialog_delete_for_everyone_label")
.foregroundStyle(conversationViewModel.selectedMessage?.message.isRetractable == true ? Color.redDanger500 : Color.gray200)
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
.disabled(conversationViewModel.selectedMessage?.message.isRetractable == false)
VStack {
Divider()
}
.frame(maxWidth: .infinity)
Button {
NotificationCenter.default.post(name: NSNotification.Name("DeleteMessageForMe"), object: nil)
if #available(iOS 16.0, *) {
if idiom != .pad {
showingSheet.toggle()
} 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)
Text("conversation_dialog_delete_locally_label")
.foregroundStyle(Color.redDanger500)
.default_text_style(styleSize: 16)
Spacer()
}
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 30)
.background(Color.gray100)
}
.padding(.bottom)
.background(Color.gray100)
.frame(maxWidth: .infinity)
.onRotate { newOrientation in
orientation = newOrientation
}
}
}
#Preview {
ConversationsListBottomSheet(showingSheet: .constant(true))
}

View file

@ -0,0 +1,184 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SwiftUI
import linphonesw
struct ConversationDocumentsListFragment: View {
@StateObject private var conversationDocumentsListViewModel = ConversationDocumentsListViewModel()
@Binding var isShowDocumentsFilesFragment: Bool
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
VStack(spacing: 1) {
Rectangle()
.foregroundStyle(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
HStack {
Image("caret-left")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
.padding(.top, 2)
.padding(.leading, -10)
.onTapGesture {
withAnimation {
isShowDocumentsFilesFragment = false
}
}
Text("conversation_document_list_title")
.multilineTextAlignment(.leading)
.default_text_style_orange_800(styleSize: 16)
Spacer()
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.padding(.horizontal)
.padding(.bottom, 4)
.background(.white)
VStack(spacing: 0) {
List {
ForEach(conversationDocumentsListViewModel.documentsList, id: \.path) { file in
DocumentRow(viewModel: conversationDocumentsListViewModel, file: file)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.onAppear {
if file == conversationDocumentsListViewModel.documentsList.last {
conversationDocumentsListViewModel.loadMoreData(totalItemsCount: conversationDocumentsListViewModel.documentsList.count)
}
}
}
}
.safeAreaInset(edge: .top, content: {
Spacer()
.frame(height: 12)
})
.listStyle(.plain)
.overlay(
VStack {
if conversationDocumentsListViewModel.documentsList.isEmpty {
Spacer()
Text("conversation_no_document_found")
.multilineTextAlignment(.leading)
.default_text_style_800(styleSize: 16)
Spacer()
}
}
.padding(.all)
)
}
.frame(maxWidth: .infinity)
}
.background(Color.gray100)
if conversationDocumentsListViewModel.operationInProgress {
PopupLoadingView()
.background(.black.opacity(0.65))
}
}
.navigationTitle("")
.navigationBarHidden(true)
.onDisappear {
withAnimation {
isShowDocumentsFilesFragment = false
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DocumentRow: View {
@ObservedObject var viewModel: ConversationDocumentsListViewModel
@State private var selectedURLAttachment: URL?
@ObservedObject var file: FileModel
var body: some View {
HStack {
VStack {
Image(getImageOfType(filename: file.fileName, type: file.mimeTypeString))
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c700)
.frame(width: 60, height: 60, alignment: .leading)
}
.frame(width: 100, height: 100)
.background(Color.grayMain2c200)
.onTapGesture {
selectedURLAttachment = URL(fileURLWithPath: file.originalPath)
}
VStack {
Text(file.fileName)
.foregroundStyle(Color.grayMain2c700)
.default_text_style_600(styleSize: 14)
.truncationMode(.middle)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
if file.fileSize > 0 {
Text(Int(file.fileSize).formatBytes())
.default_text_style_300(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
}
}
.padding(.horizontal, 10)
.frame(maxWidth: .infinity, alignment: .leading)
}
.quickLookPreview($selectedURLAttachment, in: viewModel.documentsList.compactMap { URL(fileURLWithPath: $0.originalPath) })
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
.onTapGesture {
selectedURLAttachment = URL(fileURLWithPath: file.originalPath)
}
}
func getImageOfType(filename: String, type: String) -> String {
if type == "audio/mpeg" {
return "file-audio"
} else if type == "application/pdf"
|| filename.lowercased().hasSuffix(".pdf") == true {
return "file-pdf"
} else if type.hasPrefix("text/") == true
|| ["txt", "md", "json", "xml", "csv", "log"].contains(filename.split(separator: ".").last?.lowercased()) {
return "file-text"
} else {
return "file"
}
}
}

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 != AppServices.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

@ -18,6 +18,7 @@
*/
import SwiftUI
import UniformTypeIdentifiers
// swiftlint:disable type_body_length
struct ConversationInfoFragment: View {
@ -33,6 +34,8 @@ struct ConversationInfoFragment: View {
@Binding var isMuted: Bool
@Binding var isShowEphemeralFragment: Bool
@Binding var isShowMediaFilesFragment: Bool
@Binding var isShowDocumentsFilesFragment: Bool
@Binding var isShowStartCallGroupPopup: Bool
@Binding var isShowInfoConversationFragment: Bool
@Binding var isShowEditContactFragment: Bool
@ -44,6 +47,7 @@ struct ConversationInfoFragment: View {
@Binding var isShowScheduleMeetingFragmentParticipants: [SelectedAddressModel]
@State private var participantListIsOpen = true
@State private var displayPeerAddress = false
@Binding var isShowConversationInfoPopup: Bool
@Binding var conversationInfoPopupText: String
@ -75,6 +79,13 @@ struct ConversationInfoFragment: View {
}
Spacer()
Rectangle()
.foregroundColor(.white)
.frame(width: 45, height: 45)
.onLongPressGesture(minimumDuration: 0.3) {
displayPeerAddress = true
}
}
.frame(maxWidth: .infinity)
.frame(height: 50)
@ -104,12 +115,55 @@ 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 !AppServices.corePreferences.hideSipAddresses {
Button {
UIPasteboard.general.setValue(
conversationViewModel.participantConversationModel.first?.address ?? "",
forPasteboardType: UTType.plainText.identifier
)
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
} label: {
HStack {
Text(conversationViewModel.participantConversationModel.first?.address ?? "")
.foregroundStyle(Color.grayMain2c700)
.default_text_style(styleSize: 14)
.padding(.top, 5)
Image("copy")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25)
}
}
.padding(.horizontal, 10)
}
if displayPeerAddress {
Button {
UIPasteboard.general.setValue(
conversationViewModel.peerAddress,
forPasteboardType: UTType.plainText.identifier
)
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
} label: {
HStack {
Text(conversationViewModel.peerAddress)
.foregroundStyle(Color.grayMain2c700)
.default_text_style(styleSize: 14)
.padding(.top, 5)
Image("copy")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
}
}
.padding(.horizontal, 10)
}
if !SharedMainViewModel.shared.displayedConversation!.avatarModel.lastPresenceInfo.isEmpty {
Text(SharedMainViewModel.shared.displayedConversation!.avatarModel.lastPresenceInfo)
@ -156,6 +210,31 @@ struct ConversationInfoFragment: View {
}
}
.padding(.leading, conversationViewModel.isUserAdmin ? 20 : 0)
if displayPeerAddress {
Button {
UIPasteboard.general.setValue(
conversationViewModel.peerAddress,
forPasteboardType: UTType.plainText.identifier
)
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
} label: {
HStack {
Text(conversationViewModel.peerAddress)
.foregroundStyle(Color.grayMain2c700)
.default_text_style(styleSize: 14)
.padding(.top, 5)
Image("copy")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
}
}
.padding(.horizontal, 10)
}
}
}
.frame(minHeight: 150)
@ -351,59 +430,66 @@ 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 addressConv = participantConversationModel.address
let friendIndex = contactsManager.lastSearch.firstIndex(
let friendIndex = contactsManager.lastSearch.firstIndex(
where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressConv})})
let disableAddContact = AppServices.corePreferences.disableAddContact
let hideContactEdition = AppServices.corePreferences.hideContactEdition
if (!disableAddContact || (disableAddContact && friendIndex != nil)) && !hideContactEdition {
Button(
action: {
let addressConv = participantConversationModel.address
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(
@ -517,6 +603,69 @@ struct ConversationInfoFragment: View {
}
}
Text("conversation_details_media_documents_title")
.default_text_style_800(styleSize: 18)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
.padding(.top, 20)
VStack(spacing: 0) {
Button(
action: {
withAnimation {
isShowMediaFilesFragment = true
}
},
label: {
HStack {
Image("image")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("conversation_menu_media_files")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
}
}
)
.frame(height: 60)
Divider()
Button(
action: {
withAnimation {
isShowDocumentsFilesFragment = true
}
},
label: {
HStack {
Image("file-pdf")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
Text("conversation_menu_documents_files")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
}
}
)
.frame(height: 60)
}
.padding(.horizontal, 20)
.padding(.vertical, 4)
.background(.white)
.cornerRadius(15)
.padding(.all)
Text("contact_details_actions_title")
.default_text_style_800(styleSize: 18)
.frame(maxWidth: .infinity, alignment: .leading)
@ -525,7 +674,15 @@ 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 = AppServices.corePreferences.disableAddContact
let hideContactEdition = AppServices.corePreferences.hideContactEdition
if !SharedMainViewModel.shared.displayedConversation!.isGroup && (!disableAddContact || (disableAddContact && friendIndex != nil)) && !hideContactEdition {
Button(
action: {
if SharedMainViewModel.shared.displayedConversation != nil {
@ -553,10 +710,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)
@ -699,6 +852,8 @@ struct ConversationInfoFragment: View {
ConversationInfoFragment(
isMuted: .constant(false),
isShowEphemeralFragment: .constant(false),
isShowMediaFilesFragment: .constant(false),
isShowDocumentsFilesFragment: .constant(false),
isShowStartCallGroupPopup: .constant(false),
isShowInfoConversationFragment: .constant(true),
isShowEditContactFragment: .constant(false),

View file

@ -0,0 +1,185 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SwiftUI
import linphonesw
struct ConversationMediaListFragment: View {
@StateObject private var conversationMediaListViewModel = ConversationMediaListViewModel()
@Binding var isShowMediaFilesFragment: Bool
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
VStack(spacing: 1) {
Rectangle()
.foregroundStyle(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
HStack {
Image("caret-left")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
.padding(.top, 2)
.padding(.leading, -10)
.onTapGesture {
withAnimation {
isShowMediaFilesFragment = false
}
}
Text("conversation_media_list_title")
.multilineTextAlignment(.leading)
.default_text_style_orange_800(styleSize: 16)
Spacer()
}
.frame(maxWidth: .infinity)
.frame(height: 50)
.padding(.horizontal)
.padding(.bottom, 4)
.background(.white)
ConversationMediaGridView(viewModel: conversationMediaListViewModel)
}
.background(Color.gray100)
if conversationMediaListViewModel.operationInProgress {
PopupLoadingView()
.background(.black.opacity(0.65))
}
}
.navigationTitle("")
.navigationBarHidden(true)
.onDisappear {
withAnimation {
isShowMediaFilesFragment = false
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ConversationMediaGridView: View {
@ObservedObject var viewModel: ConversationMediaListViewModel
@State private var selectedURLAttachment: URL?
private let columns = 4
private let spacing: CGFloat = 2
var body: some View {
VStack(spacing: 0) {
if !viewModel.mediaList.isEmpty {
GeometryReader { geometry in
let totalSpacing = spacing * CGFloat(columns - 1)
let itemWidth = (geometry.size.width - totalSpacing) / CGFloat(columns)
ScrollView {
LazyVGrid(
columns: Array(repeating: GridItem(.fixed(itemWidth), spacing: spacing), count: columns),
spacing: spacing
) {
ForEach(viewModel.mediaList, id: \.path) { file in
MediaGridItemView(file: file)
.aspectRatio(1, contentMode: .fit)
.frame(width: itemWidth, height: itemWidth)
.clipped()
.onTapGesture {
selectedURLAttachment = URL(fileURLWithPath: file.originalPath)
}
.onAppear {
if file == viewModel.mediaList.last {
viewModel.loadMoreData(totalItemsCount: viewModel.mediaList.count)
}
}
}
}
.padding(.horizontal, spacing)
.padding(.top, spacing)
}
}
.quickLookPreview($selectedURLAttachment, in: viewModel.mediaList.compactMap { URL(fileURLWithPath: $0.originalPath) })
} else if viewModel.mediaList.isEmpty && !viewModel.operationInProgress {
Spacer()
Text("conversation_no_media_found")
.multilineTextAlignment(.center)
.default_text_style_800(styleSize: 16)
Spacer()
} else {
Spacer()
}
}
}
}
struct MediaGridItemView: View {
@ObservedObject var file: FileModel
var body: some View {
GeometryReader { geo in
ZStack(alignment: .bottomTrailing) {
if let previewPath = file.mediaPreview,
let image = UIImage(contentsOfFile: previewPath) {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: geo.size.width, height: geo.size.height)
.clipped()
} else {
Rectangle()
.fill(Color.gray.opacity(0.2))
.frame(width: geo.size.width, height: geo.size.height)
}
if file.isVideoPreview {
Image("play-fill")
.resizable()
.renderingMode(.template)
.scaledToFit()
.frame(width: geo.size.width * 0.3, height: geo.size.height * 0.3)
.foregroundColor(.white)
.shadow(radius: 2)
.position(x: geo.size.width / 2, y: geo.size.height / 2)
}
if let duration = file.audioVideoDuration, file.isVideoPreview {
Text(duration)
.font(.caption2)
.padding(4)
.background(Color.black.opacity(0.6))
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 4))
.padding(6)
}
}
.cornerRadius(8)
}
.aspectRatio(1, contentMode: .fit)
}
}

View file

@ -25,13 +25,14 @@ struct ConversationsFragment: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@State var showingSheet: Bool = false
@Binding var text: String
@State var showingSheet: Bool = false
var body: some View {
ZStack {
if #available(iOS 16.0, *), idiom != .pad {
ConversationsListFragment(showingSheet: $showingSheet, text: $text)
ConversationsListFragment(text: $text, showingSheet: $showingSheet)
.sheet(isPresented: $showingSheet) {
ConversationsListBottomSheet(
showingSheet: $showingSheet
@ -43,7 +44,7 @@ struct ConversationsFragment: View {
)
}
} else {
ConversationsListFragment(showingSheet: $showingSheet, text: $text)
ConversationsListFragment(text: $text, showingSheet: $showingSheet)
.halfSheet(showSheet: $showingSheet) {
ConversationsListBottomSheet(
showingSheet: $showingSheet

View file

@ -26,10 +26,12 @@ struct ConversationsListFragment: View {
@EnvironmentObject var navigationManager: NavigationManager
@ObservedObject var contactsManager = ContactsManager.shared
@EnvironmentObject var conversationsListViewModel: ConversationsListViewModel
@Binding var showingSheet: Bool
@Binding var text: String
@Binding var showingSheet: Bool
var body: some View {
VStack {
@ -42,6 +44,34 @@ struct ConversationsListFragment: View {
text: $text
)
}
if !conversationsListViewModel.currentFilter.isEmpty {
if !contactsManager.lastSearch.isEmpty {
HStack(alignment: .center) {
Text("contacts_list_all_contacts_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
}
ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in
withAnimation {
conversationsListViewModel.createOneToOneChatRoomWith(remote: addr)
}
})
if !contactsManager.lastSearchSuggestions.isEmpty {
HStack(alignment: .center) {
Text("generic_address_picker_suggestions_list_title")
.default_text_style_800(styleSize: 16)
Spacer()
}
suggestionsList
}
}
}
.safeAreaInset(edge: .top, content: {
Spacer()
@ -50,7 +80,13 @@ struct ConversationsListFragment: View {
.listStyle(.plain)
.overlay(
VStack {
if conversationsListViewModel.conversationsList.isEmpty {
if conversationsListViewModel.conversationsList.isEmpty &&
(
conversationsListViewModel.currentFilter.isEmpty ||
(!conversationsListViewModel.currentFilter.isEmpty &&
contactsManager.lastSearch.isEmpty &&
contactsManager.lastSearchSuggestions.isEmpty)
) {
Spacer()
Image("illus-belledonne")
.resizable()
@ -65,6 +101,11 @@ struct ConversationsListFragment: View {
}
.padding(.all)
)
.onDisappear {
if !conversationsListViewModel.currentFilter.isEmpty {
conversationsListViewModel.resetFilterConversations()
}
}
}
.navigationTitle("")
.navigationBarHidden(true)
@ -77,6 +118,69 @@ struct ConversationsListFragment: View {
}
}
}
var suggestionsList: some View {
ForEach(0..<contactsManager.lastSearchSuggestions.count, id: \.self) { index in
Button {
if let address = contactsManager.lastSearchSuggestions[index].address {
withAnimation {
conversationsListViewModel.createOneToOneChatRoomWith(remote: address)
}
}
} label: {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil {
if contactsManager.lastSearchSuggestions[index].address!.domain != AppServices.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)
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text("username_error")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
}
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
}
}
struct ConversationRow: View {
@ -105,14 +209,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()
}
@ -141,11 +277,11 @@ struct ConversationRow: View {
}
if !conversation.encryptionEnabled {
Image("lock-simple-open-bold")
Image("lock-simple-open")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeWarning600)
.frame(width: 18, height: 18, alignment: .trailing)
.frame(width: 16, height: 16, alignment: .trailing)
}
if conversation.isEphemeral {
@ -212,7 +348,7 @@ struct ConversationRow: View {
#Preview {
ConversationsListFragment(
showingSheet: .constant(false),
text: .constant("")
text: .constant(""),
showingSheet: .constant(false)
)
}

View file

@ -32,6 +32,9 @@ struct StartConversationFragment: View {
@Binding var isShowStartConversationFragment: Bool
@State private var contactAvatarModel: ContactAvatarModel? = nil
@State private var isShowSipAddressesPopup: Bool = false
@FocusState var isSearchFieldFocused: Bool
@State private var delayedColor = Color.white
@ -170,34 +173,59 @@ 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
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)
ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in
CoreContext.shared.doOnCoreQueue { core in
ContactAvatarModel.getAvatarModelFromAddress(address: addr) { contactAvatarModel in
self.contactAvatarModel = contactAvatarModel
DispatchQueue.main.async {
if contactAvatarModel.addresses.count == 1 && contactAvatarModel.phoneNumbersWithLabel.isEmpty {
startConversationViewModel.createOneToOneChatRoomWith(remote: addr)
} else if contactAvatarModel.addresses.isEmpty && contactAvatarModel.phoneNumbersWithLabel.count == 1 {
if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let phoneAddr = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) {
startConversationViewModel.createOneToOneChatRoomWith(remote: phoneAddr)
}
} else {
DispatchQueue.main.async {
isShowSipAddressesPopup = true
}
}
}
}
}
})
.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))
}
}
}
@ -205,6 +233,100 @@ struct StartConversationFragment: View {
}
.background(.white)
if isShowSipAddressesPopup && contactAvatarModel != nil {
VStack(alignment: .leading) {
HStack {
Text("contact_dialog_pick_phone_number_or_sip_address_title")
.default_text_style_800(styleSize: 16)
.padding(.bottom, 2)
Spacer()
Image("x")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.padding(.all, 10)
}
.frame(maxWidth: .infinity)
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()
}
.padding(.vertical, 15)
.padding(.horizontal, 10)
}
.background(.white)
.onTapGesture {
do {
let addr = try Factory.Instance.createAddress(addr: contactAvatarModel!.addresses[index])
startConversationViewModel.createOneToOneChatRoomWith(remote: addr)
} catch {
Log.error("[StartConversationFragment] unable to create address for a new outgoing call : \(contactAvatarModel!.addresses[index]) \(error) ")
}
}
}
ForEach(0..<contactAvatarModel!.phoneNumbersWithLabel.count, id: \.self) { index in
HStack {
HStack {
VStack {
Text(String(localized: "phone_number") + ":")
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
Text(contactAvatarModel!.phoneNumbersWithLabel[index].phoneNumber)
.default_text_style(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
}
.padding(.vertical, 15)
.padding(.horizontal, 10)
}
.background(.white)
.onTapGesture {
CoreContext.shared.doOnCoreQueue { core in
if let phoneAddr = core.interpretUrl(url: contactAvatarModel!.phoneNumbersWithLabel[index].phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) {
DispatchQueue.main.async {
startConversationViewModel.createOneToOneChatRoomWith(remote: phoneAddr)
}
} else {
Log.error("[StartConversationFragment] unable to create address (interpret Url for phone number) for a new outgoing call : \(contactAvatarModel!.addresses[index])")
}
}
}
}
}
.padding(.horizontal, 20)
.padding(.vertical, 20)
.background(.white)
.cornerRadius(20)
.frame(maxHeight: .infinity)
.shadow(color: Color.orangeMain500, radius: 0, x: 0, y: 2)
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
.padding(.horizontal, 20)
.background(.black.opacity(0.65))
.zIndex(3)
.onTapGesture {
isShowSipAddressesPopup.toggle()
}
}
if startConversationViewModel.operationOneToOneInProgress {
PopupLoadingView()
.background(.black.opacity(0.65))
@ -257,6 +379,7 @@ struct StartConversationFragment: View {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil {
if contactsManager.lastSearchSuggestions[index].address!.domain != AppServices.corePreferences.defaultDomain {
Image(uiImage: contactsManager.textToImage(
firstName: String(contactsManager.lastSearchSuggestions[index].address!.asStringUriOnly().dropFirst(4)),
lastName: ""))
@ -266,9 +389,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

@ -135,7 +135,7 @@ struct UIList: UIViewRepresentable {
tableView.backgroundColor = UIColor(.white)
tableView.scrollsToTop = true
if SharedMainViewModel.shared.displayedConversation != nil && SharedMainViewModel.shared.displayedConversation!.encryptionEnabled {
if SharedMainViewModel.shared.displayedConversation != nil {
let footerView = Self.makeFooterView()
footerView.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 80)
footerView.transform = CGAffineTransformMakeScale(1, -1)
@ -180,25 +180,26 @@ struct UIList: UIViewRepresentable {
}
static func makeFooterView() -> UIView {
let encryptionEnabled = SharedMainViewModel.shared.displayedConversation!.encryptionEnabled
let host = UIHostingController(
rootView:
VStack {
HStack {
Image("lock-simple-bold")
Image(encryptionEnabled ? "lock-simple-bold" : "lock-simple-open")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.blueInfo500)
.foregroundStyle(encryptionEnabled ? Color.blueInfo500 : Color.orangeWarning600)
.frame(width: 25, height: 25)
.padding(10)
VStack(spacing: 5) {
Text("conversation_end_to_end_encrypted_event_title")
.foregroundStyle(Color.blueInfo500)
Text(encryptionEnabled ? "conversation_end_to_end_encrypted_event_title" : "conversation_warning_disabled_because_not_secured_title")
.foregroundStyle(encryptionEnabled ? Color.blueInfo500 : Color.orangeWarning600)
.default_text_style_700(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
Text("conversation_end_to_end_encrypted_event_subtitle")
Text(encryptionEnabled ? "conversation_end_to_end_encrypted_event_subtitle" : "conversation_warning_disabled_because_not_secured_subtitle")
.foregroundStyle(Color.gray400)
.default_text_style(styleSize: 12)
.frame(maxWidth: .infinity, alignment: .leading)
@ -210,7 +211,7 @@ struct UIList: UIViewRepresentable {
.overlay(
RoundedRectangle(cornerRadius: 10)
.inset(by: 0.5)
.stroke(Color.blueInfo500, lineWidth: 0.5)
.stroke(encryptionEnabled ? Color.blueInfo500 : Color.orangeWarning600, lineWidth: 0.5)
)
.padding(.horizontal, 10)
}
@ -551,6 +552,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 },

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