Compare commits
191 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
156231f8aa | ||
|
|
0132c5253f | ||
|
|
828fadb9e0 | ||
|
|
9524c6f2f3 | ||
|
|
8a612be0a0 | ||
|
|
127e12b384 | ||
|
|
b84bd1faf3 | ||
|
|
54b8ae4b02 | ||
|
|
5615d51e27 | ||
|
|
fed6394cd5 | ||
|
|
b82156d2f2 | ||
|
|
83acef02d7 | ||
|
|
068d2d2902 | ||
|
|
1943f0f21c | ||
|
|
1ffcd3e1ae | ||
|
|
c01f79dd20 | ||
|
|
2f56839937 | ||
|
|
0fe2b4370f | ||
|
|
fa9be23c2d | ||
|
|
b8efad4980 | ||
|
|
d36a0a7081 | ||
|
|
9555245ede | ||
|
|
231035f109 | ||
|
|
1bb372741e | ||
|
|
9d78fc2e29 | ||
|
|
d0418ae07c | ||
|
|
d00257d93c | ||
|
|
58d269a65f | ||
|
|
95a7f0987a | ||
|
|
e1d3df577f | ||
|
|
3b10fbb7ce | ||
|
|
fd2730661d | ||
|
|
66f2ac6fdc | ||
|
|
6c5bf43062 | ||
|
|
9364e7f196 | ||
|
|
cdde88e32a | ||
|
|
e066ff4ee7 | ||
|
|
bfb4ac3c22 | ||
|
|
75e96ed8a5 | ||
|
|
9cc8923e3f | ||
|
|
db9c9f1834 | ||
|
|
ef09f6c412 | ||
|
|
ca9f5ffe1f | ||
|
|
5bbbbe5d75 | ||
|
|
f08bb865aa | ||
|
|
0f6d8b1296 | ||
|
|
d39e4a0e34 | ||
|
|
23f5211131 | ||
|
|
1fff8d6d36 | ||
|
|
d9d2d24d85 | ||
|
|
09863890ea | ||
|
|
d5d1600b4e | ||
|
|
cf97da11b1 | ||
|
|
304651d776 | ||
|
|
b753b5925e | ||
|
|
112d7bbaa9 | ||
|
|
8223d20fc6 | ||
|
|
9ac0445347 | ||
|
|
2287e9a45a | ||
|
|
3eb09bf8b0 | ||
|
|
e3bf00dca5 | ||
|
|
b3ed027c61 | ||
|
|
2fea9d720e | ||
|
|
8d0f638d46 | ||
|
|
9072119031 | ||
|
|
d25c19ff8e | ||
|
|
e64c0611fb | ||
|
|
dc5f131cec | ||
|
|
02c0509c98 | ||
|
|
ceca9acc21 | ||
|
|
6b93a7ef5e | ||
|
|
ac5a23bfff | ||
|
|
b348307b6c | ||
|
|
c5c970d177 | ||
|
|
986276c04f | ||
|
|
221f0b10d7 | ||
|
|
aed6876065 | ||
|
|
4f7e4b0c36 | ||
|
|
6575a4b0f2 | ||
|
|
9723c0de04 | ||
|
|
990d2f36af | ||
|
|
8d5c0ce79b | ||
|
|
61931138b2 | ||
|
|
1dbbe6a53d | ||
|
|
4c45a9bb1e | ||
|
|
4feae7fccd | ||
|
|
1501dae019 | ||
|
|
84ec09173c | ||
|
|
4cd63b53b2 | ||
|
|
d4b10d38ae | ||
|
|
6827bdc1dc | ||
|
|
ed08190ff4 | ||
|
|
57b7b857bc | ||
|
|
f16a0f42ae | ||
|
|
c7ddf2d8d0 | ||
|
|
7decb12a3f | ||
|
|
da1abd75ae | ||
|
|
33b379285f | ||
|
|
b45a328ad4 | ||
|
|
c9f2915ca0 | ||
|
|
fe8432f128 | ||
|
|
36fa752ccf | ||
|
|
0f8df65dff | ||
|
|
221e3cbb4b | ||
|
|
2b64c26518 | ||
|
|
5492a3e3a9 | ||
|
|
773102e4bd | ||
|
|
b462657a77 | ||
|
|
5d13a2b49d | ||
|
|
7bdb8fa92d | ||
|
|
0daba4fe03 | ||
|
|
07dbf407b0 | ||
|
|
7972fd7c1f | ||
|
|
fa1f8386b4 | ||
|
|
a421d90d0c | ||
|
|
b904f71f79 | ||
|
|
e748a001bf | ||
|
|
4fbb43f38c | ||
|
|
bcee4439f5 | ||
|
|
b49445d50d | ||
|
|
a3d2c74592 | ||
|
|
4f6dc2c0c7 | ||
|
|
08ab9a1078 | ||
|
|
db72bdf242 | ||
|
|
9413f6f5dc | ||
|
|
7237a5f4a7 | ||
|
|
8c9784a21d | ||
|
|
bb4134ede0 | ||
|
|
8cd322ff0e | ||
|
|
f8b7e3b319 | ||
|
|
aefa334038 | ||
|
|
9559701a5e | ||
|
|
5bb757d150 | ||
|
|
ac6b478eb1 | ||
|
|
41f9db8199 | ||
|
|
4b3d99245f | ||
|
|
aae7c290be | ||
|
|
0a06644473 | ||
|
|
b75db70d19 | ||
|
|
9beadaadd9 | ||
|
|
888b75a2d4 | ||
|
|
068b93cf1c | ||
|
|
e9eebbd45a | ||
|
|
1c3680df65 | ||
|
|
1389565b83 | ||
|
|
4fde1933dd | ||
|
|
a33a8666ae | ||
|
|
3b2ce2ed4b | ||
|
|
4cf1dbd8b5 | ||
|
|
1d0df11c61 | ||
|
|
ee1c09e98f | ||
|
|
8bd84ca8a5 | ||
|
|
1e53619eaa | ||
|
|
112029d0df | ||
|
|
8737bcb40d | ||
|
|
5837e5e85d | ||
|
|
e81699052a | ||
|
|
147682a0e5 | ||
|
|
0dfdb5551c | ||
|
|
880967d2e3 | ||
|
|
b36f220911 | ||
|
|
a2564c4cc0 | ||
|
|
d3984b4b1a | ||
|
|
91c6f7a311 | ||
|
|
de9656d94d | ||
|
|
43d63ae081 | ||
|
|
c7f7606859 | ||
|
|
54ca929f88 | ||
|
|
c6291f61d6 | ||
|
|
42997d6891 | ||
|
|
014e7c575e | ||
|
|
773696260b | ||
|
|
304f46ba02 | ||
|
|
e0229fde0f | ||
|
|
108df56148 | ||
|
|
c7a9edf25f | ||
|
|
90f2ad7e58 | ||
|
|
1485e7a574 | ||
|
|
8f131ad335 | ||
|
|
bc1c737973 | ||
|
|
926107061a | ||
|
|
3aa9419c5d | ||
|
|
a5bef93587 | ||
|
|
525e705b59 | ||
|
|
a7e2ebb600 | ||
|
|
14635250f8 | ||
|
|
357418287c | ||
|
|
3c56f09130 | ||
|
|
5b6e2e1c0a | ||
|
|
53a135a4b6 | ||
|
|
5035c6a924 |
16
Base.lproj/AppIntentVocabulary.plist
Normal 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>
|
||||
117
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "lock-simple-open-bold.svg",
|
||||
"filename" : "arrow-right.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
1
Linphone/Assets.xcassets/arrow-right.imageset/arrow-right.svg
vendored
Normal 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 |
|
|
@ -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 |
21
Linphone/Assets.xcassets/layout.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
1
Linphone/Assets.xcassets/layout.imageset/layout.svg
vendored
Normal 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 |
21
Linphone/Assets.xcassets/list.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
1
Linphone/Assets.xcassets/list.imageset/list.svg
vendored
Normal 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 |
|
|
@ -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 |
|
|
@ -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 |
21
Linphone/Assets.xcassets/music-notes.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
1
Linphone/Assets.xcassets/music-notes.imageset/music-notes.svg
vendored
Normal 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 |
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "not_trusted.svg",
|
||||
"filename" : "not-trusted.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
|
|
|
|||
59
Linphone/Assets.xcassets/not-trusted.imageset/not-trusted.svg
vendored
Normal 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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
21
Linphone/Assets.xcassets/trash.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
1
Linphone/Assets.xcassets/trash.imageset/trash.svg
vendored
Normal 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 |
21
Linphone/Assets.xcassets/voicemail.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
1
Linphone/Assets.xcassets/voicemail.imageset/voicemail.svg
vendored
Normal 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 |
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
Linphone/GeneratedGit.swift
Normal 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"
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
44
Linphone/Launch Screen.storyboard
Normal 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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
Linphone/Localizable/ca.lproj/Localizable.strings
Normal 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";
|
||||
|
|
@ -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é)";
|
||||
|
|
|
|||
|
|
@ -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 d’autres 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 à l’abri 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 & 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" = "";
|
||||
|
|
|
|||
|
|
@ -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 & 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.";
|
||||
|
|
|
|||
163
Linphone/Localizable/es.lproj/Localizable.strings
Normal 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";
|
||||
17
Linphone/Localizable/eu.lproj/Localizable.strings
Normal 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.";
|
||||
32
Linphone/Localizable/fi.lproj/Localizable.strings
Normal 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)";
|
||||
|
|
@ -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" = "J’ai 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 l’appel";
|
||||
"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 l’appareil %2$@ de %1$@ ?\nVoulez-vous passer l’appel ?";
|
||||
"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 d’utilisateur";
|
||||
"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.";
|
||||
|
|
|
|||
87
Linphone/Localizable/hu.lproj/Localizable.strings
Normal 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 email–cí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 QR–kód!";
|
||||
"assistant_scan_qr_code" = "QR–kó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";
|
||||
1
Linphone/Localizable/it.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
2
Linphone/Localizable/mk.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
"notification_earpiece_enforcement_message" = "Ве молиме користете само слушалка. Другите аудио излези се оневозможени.";
|
||||
410
Linphone/Localizable/nl.lproj/Localizable.strings
Normal 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.";
|
||||
71
Linphone/Localizable/pl.lproj/Localizable.strings
Normal 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";
|
||||
526
Linphone/Localizable/pt-BR.lproj/Localizable.strings
Normal 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 d’autres 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 à l’abri 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";
|
||||
9
Linphone/Localizable/pt.lproj/Localizable.strings
Normal 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.";
|
||||
|
|
@ -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" = "**Доступ к камере:** Для видеозвонков и видеоконференций.";
|
||||
|
|
|
|||
385
Linphone/Localizable/sk.lproj/Localizable.strings
Normal 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)";
|
||||
|
|
@ -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" = "Ідентифікатор автентифікації (якщо відрізняється)";
|
||||
|
|
|
|||
|
|
@ -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(不能为空) 过滤";
|
||||
|
|
|
|||
BIN
Linphone/Ressources/Sounds/incoming_chat.wav
Normal 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
81
Linphone/TelecomManager/EarlyPushkitDelegate.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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!)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
34
Linphone/UI/Main/Contacts/Model/ContactDeviceModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ struct ConversationsView: View {
|
|||
|
||||
#Preview {
|
||||
ConversationsListFragment(
|
||||
showingSheet: .constant(false),
|
||||
text: .constant("")
|
||||
text: .constant(""),
|
||||
showingSheet: .constant(false)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||