mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-09 09:27:42 +02:00
Merge branch 'release/v2.6.0'
This commit is contained in:
@@ -17,7 +17,7 @@ jobs:
|
||||
add_to_triage:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.repository == 'vector-im/element-x-ios'
|
||||
github.repository == 'vector-im/element-ios'
|
||||
steps:
|
||||
- uses: octokit/graphql-action@v2.x
|
||||
with:
|
||||
|
||||
+52
@@ -1,3 +1,55 @@
|
||||
## Changes in 1.10.9 (2023-04-04)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Encryption: Simplify event encryption decoration ([#7440](https://github.com/vector-im/element-ios/pull/7440))
|
||||
- Add user suggestions for @room and highlight incoming messages containing @room when the room is encrypted. ([#7453](https://github.com/vector-im/element-ios/pull/7453))
|
||||
- Crypto: Expand rust crypto rollout to 50% users ([#7466](https://github.com/vector-im/element-ios/pull/7466))
|
||||
- Upgrade MatrixSDK version ([v0.26.6](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.6)).
|
||||
- Replace Terms and Conditions with Acceptable Use Policy. ([#7456](https://github.com/vector-im/element-ios/issues/7456))
|
||||
- Crypto: Display correct SDK version ([#7457](https://github.com/vector-im/element-ios/issues/7457))
|
||||
|
||||
🐛 Bugfixes
|
||||
|
||||
- QR verification: Start scanning as soon as camera ready ([#7469](https://github.com/vector-im/element-ios/pull/7469))
|
||||
- Timeline: No event decoration if no decryption result ([#7471](https://github.com/vector-im/element-ios/pull/7471))
|
||||
- Long pills are now truncated. ([#7413](https://github.com/vector-im/element-ios/issues/7413))
|
||||
- Update the read marker position even if it is not displayed ([#7420](https://github.com/vector-im/element-ios/issues/7420))
|
||||
|
||||
|
||||
## Changes in 1.10.8 (2023-03-28)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Verification: Display upgrade verification prompt ([#7454](https://github.com/vector-im/element-ios/pull/7454))
|
||||
- Upgrade MatrixSDK version ([v0.26.5](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.5)).
|
||||
|
||||
|
||||
## Changes in 1.10.7 (2023-03-22)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Upgrade MatrixSDK version ([v0.26.4](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.4)).
|
||||
|
||||
|
||||
## Changes in 1.10.6 (2023-03-21)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Encryption: Refactor user / room encryption trust level ([#7430](https://github.com/vector-im/element-ios/pull/7430))
|
||||
- Crypto: Increase local rust crypto rollout to 20% of all users ([#7434](https://github.com/vector-im/element-ios/pull/7434))
|
||||
- Upgrade MatrixSDK version ([v0.26.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.2)).
|
||||
- Permalinks to a room/space are pillified ([#7409](https://github.com/vector-im/element-ios/issues/7409))
|
||||
- Permalinks to a matrix user are pillified ([#7411](https://github.com/vector-im/element-ios/issues/7411))
|
||||
- Permalinks to messages are pillified ([#7412](https://github.com/vector-im/element-ios/issues/7412))
|
||||
- Loading: Update startup progress UX ([#7417](https://github.com/vector-im/element-ios/issues/7417))
|
||||
|
||||
🐛 Bugfixes
|
||||
|
||||
- Room list: increase tappability area of the avatar button. ([#7427](https://github.com/vector-im/element-ios/pull/7427))
|
||||
- Manage bad m.file attachment format. ([#7406](https://github.com/vector-im/element-ios/issues/7406))
|
||||
|
||||
|
||||
## Changes in 1.10.5 (2023-03-13)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
+30
-1
@@ -1,4 +1,33 @@
|
||||
Changes in BWI project 2.4.0 (2023-03-14)
|
||||
Changes in BWI project 2.5.0 (2023-05-09)
|
||||
===================================================
|
||||
|
||||
Upstream merge ✨:
|
||||
|
||||
- v1.10.9
|
||||
|
||||
Features ✨:
|
||||
- New device Manager (#4516)
|
||||
|
||||
Improvements 🙌:
|
||||
- Matomo tracking consent (#4450)
|
||||
- Matomo tracking Server configuration (#4454)
|
||||
- Matomo tracking of encrypted messages (#4415)
|
||||
- Possibility to block requests on server maintenance (#4174)
|
||||
- enable rust encryption for beta app
|
||||
|
||||
Bugfix 🐛:
|
||||
- Reset matomo tracking id on logout (#4494)
|
||||
- Fix wrong behaviour in server selection (#4643)
|
||||
|
||||
Translations 🗣 :
|
||||
- English translations onboarding (#4377)
|
||||
|
||||
SDK API changes ⚠️:
|
||||
|
||||
Build 🧱:
|
||||
|
||||
|
||||
Changes in BWI project 2.5.0 (2023-04-16)
|
||||
===================================================
|
||||
|
||||
Upstream merge ✨:
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
|
||||
|
||||
// App identity
|
||||
BUNDLE_DISPLAY_NAME = Messenger
|
||||
BASE_BUNDLE_IDENTIFIER = de.bwi.messenger
|
||||
APPLICATION_GROUP_IDENTIFIER = group.de.messenger
|
||||
APPLICATION_SCHEME = element
|
||||
BUNDLE_DISPLAY_NAME = BuM-Beta
|
||||
BASE_BUNDLE_IDENTIFIER = de.bwi.messenger-beta
|
||||
APPLICATION_GROUP_IDENTIFIER = group.de.messenger-beta
|
||||
APPLICATION_SCHEME = element
|
||||
|
||||
// Team
|
||||
DEVELOPMENT_TEAM = Q111Q11QQ1
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
//
|
||||
|
||||
// Version
|
||||
MARKETING_VERSION = 2.5.0
|
||||
MARKETING_VERSION = 2.6.0
|
||||
CURRENT_PROJECT_VERSION = 20220714163152
|
||||
|
||||
@@ -183,7 +183,9 @@ class BWIBuildSettings: NSObject {
|
||||
"3740163f98aeda7dba285d2af1bfc351db395868268e2759ca701f926a6605a5",
|
||||
"240b05d9a54999140d23f21d104209fbc5e5179366ba3a7e58c8fad763aa88bd",
|
||||
"4d5b6dcf02396274be58a69c4bbeba975b529f6b19c504fc99a37892ee1cf0b5",
|
||||
"0d157119821bd9d76ac4f24c7f44f56e6bb5b766a6d5ee7dad6634420e79271a"
|
||||
"0d157119821bd9d76ac4f24c7f44f56e6bb5b766a6d5ee7dad6634420e79271a",
|
||||
"e3573fe09d518cce80cececedf80f8e0020cbc150f22db8b64827bff2e27abd9",
|
||||
"b76a62ccd8ea70d01c3a35ec3839e49ed2c83c8e3276f40a1b2c2cdf7cd77d01"
|
||||
]
|
||||
|
||||
// use a different badge color if the user was mentioned in a room
|
||||
@@ -239,7 +241,7 @@ class BWIBuildSettings: NSObject {
|
||||
var applicationWebAppUrlString = ""
|
||||
|
||||
// Default servers proposed on the authentication screen
|
||||
var serverConfigDefaultHomeserverUrlString = ""
|
||||
var serverConfigDefaultHomeserverUrlString = "https://bundesmessenger.isthebest.real"
|
||||
var serverConfigDefaultIdentityServerUrlString = ""
|
||||
var serverConfigPreSelections = ["":""]
|
||||
var serverConfigSygnalAPIUrlString = "http://push-local/_matrix/push/v1/notify"
|
||||
@@ -248,6 +250,7 @@ class BWIBuildSettings: NSObject {
|
||||
var applicationCopyrightUrlString = "https://messenger.bwi.de/copyright"
|
||||
var applicationPrivacyPolicyUrlString = "https://messenger.bwi.de/datenschutz"
|
||||
var applicationTermsConditionsUrlString = ""
|
||||
var applicationPrivacyPolicyWithMatomoSectionUrlString = "https://messenger.bwi.de/datenschutz#c6637"
|
||||
|
||||
// MARk: - Matrix permalinks
|
||||
// Paths for URLs that will considered as Matrix permalinks. Those permalinks are opened within the app
|
||||
@@ -457,6 +460,7 @@ class BWIBuildSettings: NSObject {
|
||||
var onboardingEnableNewAuthenticationFlow = true
|
||||
// show app specific welcoming screens
|
||||
var onboardingShowWelcomeScreens = true
|
||||
var disableLegacyAuthentication = true
|
||||
|
||||
// ---
|
||||
|
||||
@@ -485,8 +489,8 @@ class BWIBuildSettings: NSObject {
|
||||
var passwordIndicatorOnLogin = true
|
||||
|
||||
// MARK: Displays the element base version on the settings screen
|
||||
var elementBaseVersion = "1.10.5"
|
||||
|
||||
var elementBaseVersion = "1.10.9"
|
||||
|
||||
var showElementBaseVersion = true
|
||||
|
||||
// MARK: Bypasses the normal forgot password process by presenting the user an information alert
|
||||
@@ -531,8 +535,15 @@ class BWIBuildSettings: NSObject {
|
||||
var bwiShowHappyBirthdayCampaign = false
|
||||
var bwiHappyBirthdayCampaignIdentifier: String = "2022"
|
||||
var bwiDisableSecuritySettingsUntrustedDevices = true
|
||||
|
||||
|
||||
// MARK: - analytics with matomo
|
||||
var bwiMatomoTrackingDefaultState = false
|
||||
var bwiMatomoEnabled = false
|
||||
var matomoNameProd = ""
|
||||
var matomoNameBeta = ""
|
||||
var matomoServerProd = ""
|
||||
|
||||
var bwiShowNewFeatures = true
|
||||
|
||||
// MARK: - Message Bubbles bwi show in our menu and only for beta builds
|
||||
@@ -599,4 +610,13 @@ class BWIBuildSettings: NSObject {
|
||||
|
||||
// MARK: Client error search term
|
||||
var clientErrorSearchTerm = "Request failed: client error"
|
||||
|
||||
// MARK: Device Manager
|
||||
var deviceManagerShowIPAddress = false
|
||||
|
||||
// MARK: Rust Encryption
|
||||
var useRustEncryption = false
|
||||
|
||||
// MARK: Sessions Manager
|
||||
var enableNewSessionManagerByDefault = false
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ extension BWIBuildSettings {
|
||||
locationSharingEnabled = false
|
||||
bwiLocationShareButtonVisible = false
|
||||
bwiLoginFlowLayout = false
|
||||
useRustEncryption = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ extension BWIBuildSettings {
|
||||
bwiLocationShareButtonVisible = false
|
||||
bwiLoginFlowLayout = false
|
||||
authScreenShowTestServerOptions = false
|
||||
|
||||
enableNewSessionManagerByDefault = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ final class BuildSettings: NSObject {
|
||||
// Note: Set empty strings to hide the related entry in application settings
|
||||
static let applicationCopyrightUrlString = "https://element.io/copyright"
|
||||
static let applicationPrivacyPolicyUrlString = "https://element.io/privacy"
|
||||
static let applicationTermsConditionsUrlString = "https://element.io/terms-of-service"
|
||||
static let applicationAcceptableUsePolicyUrlString = "https://element.io/acceptable-use-policy-terms"
|
||||
static let applicationHelpUrlString =
|
||||
"https://element.io/help"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
source 'https://cdn.cocoapods.org/'
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '14.0'
|
||||
@@ -16,7 +16,7 @@ use_frameworks!
|
||||
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
|
||||
#
|
||||
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
|
||||
$matrixSDKVersion = '= 0.26.1'
|
||||
$matrixSDKVersion = '= 0.26.6'
|
||||
# $matrixSDKVersion = :local
|
||||
# $matrixSDKVersion = { :branch => 'develop'}
|
||||
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
|
||||
@@ -43,7 +43,7 @@ when String # specific MatrixSDK released version
|
||||
$matrixSDKVersionSpec = $matrixSDKVersion
|
||||
end
|
||||
|
||||
$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.1_bwi' }
|
||||
$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.6.1_bwi' }
|
||||
|
||||
# Method to import the MatrixSDK
|
||||
def import_MatrixSDK
|
||||
|
||||
+19
-15
@@ -27,7 +27,9 @@ PODS:
|
||||
- GBDeviceInfo/Core (7.1.0)
|
||||
- GZIP (1.3.0)
|
||||
- Introspect (0.1.4)
|
||||
- JitsiMeetSDK (5.0.2)
|
||||
- JitsiMeetSDKLite (7.0.1-lite):
|
||||
- JitsiWebRTC (~> 106.0)
|
||||
- JitsiWebRTC (106.0.0)
|
||||
- KeychainAccess (4.2.2)
|
||||
- KituraContracts (1.2.1):
|
||||
- LoggerAPI (~> 1.7)
|
||||
@@ -37,20 +39,20 @@ PODS:
|
||||
- LoggerAPI (1.9.200):
|
||||
- Logging (~> 1.1)
|
||||
- Logging (1.4.0)
|
||||
- MatrixSDK (0.26.1):
|
||||
- MatrixSDK/Core (= 0.26.1)
|
||||
- MatrixSDK/Core (0.26.1):
|
||||
- MatrixSDK (0.26.6):
|
||||
- MatrixSDK/Core (= 0.26.6)
|
||||
- MatrixSDK/Core (0.26.6):
|
||||
- AFNetworking (~> 4.0.0)
|
||||
- GZIP (~> 1.3.0)
|
||||
- libbase58 (~> 0.1.4)
|
||||
- MatrixSDKCrypto (= 0.2.1)
|
||||
- MatrixSDKCrypto (= 0.3.2)
|
||||
- OLMKit (~> 3.2.5)
|
||||
- Realm (= 10.27.0)
|
||||
- SwiftyBeaver (= 1.9.5)
|
||||
- MatrixSDK/JingleCallStack (0.26.1):
|
||||
- JitsiMeetSDK (= 5.0.2)
|
||||
- MatrixSDK/JingleCallStack (0.26.6):
|
||||
- JitsiMeetSDKLite (= 7.0.1-lite)
|
||||
- MatrixSDK/Core
|
||||
- MatrixSDKCrypto (0.2.1)
|
||||
- MatrixSDKCrypto (0.3.2)
|
||||
- OLMKit (3.2.12):
|
||||
- OLMKit/olmc (= 3.2.12)
|
||||
- OLMKit/olmcpp (= 3.2.12)
|
||||
@@ -100,8 +102,8 @@ DEPENDENCIES:
|
||||
- KeychainAccess (~> 4.2.2)
|
||||
- KTCenterFlowLayout (~> 1.3.1)
|
||||
- libPhoneNumber-iOS (~> 0.9.13)
|
||||
- MatrixSDK (= 0.26.1)
|
||||
- MatrixSDK/JingleCallStack (= 0.26.1)
|
||||
- MatrixSDK (= 0.26.6)
|
||||
- MatrixSDK/JingleCallStack (= 0.26.6)
|
||||
- OLMKit
|
||||
- PostHog (~> 2.0.0)
|
||||
- ReadMoreTextView (~> 3.0.1)
|
||||
@@ -133,7 +135,8 @@ SPEC REPOS:
|
||||
- GBDeviceInfo
|
||||
- GZIP
|
||||
- Introspect
|
||||
- JitsiMeetSDK
|
||||
- JitsiMeetSDKLite
|
||||
- JitsiWebRTC
|
||||
- KeychainAccess
|
||||
- KituraContracts
|
||||
- KTCenterFlowLayout
|
||||
@@ -175,7 +178,8 @@ SPEC CHECKSUMS:
|
||||
GBDeviceInfo: 5d62fa85bdcce3ed288d83c28789adf1173e4376
|
||||
GZIP: 416858efbe66b41b206895ac6dfd5493200d95b3
|
||||
Introspect: b62c4dd2063072327c21d618ef2bedc3c87bc366
|
||||
JitsiMeetSDK: edcac8e2b92ee0c7f3e75bd0aefefbe9faccfc93
|
||||
JitsiMeetSDKLite: d59573336ce887ec52327a9927aa8443f560d0b9
|
||||
JitsiWebRTC: f441eb0e2d67f0588bf24e21c5162e97342714fb
|
||||
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||
KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b
|
||||
KTCenterFlowLayout: 6e02b50ab2bd865025ae82fe266ed13b6d9eaf97
|
||||
@@ -183,8 +187,8 @@ SPEC CHECKSUMS:
|
||||
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
|
||||
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
|
||||
Logging: beeb016c9c80cf77042d62e83495816847ef108b
|
||||
MatrixSDK: 0a371af6c33ef20c9c5000cf2badf3d600c25d26
|
||||
MatrixSDKCrypto: 477d818bf2cc37b6cf702a290eb647bc8cf3cb1b
|
||||
MatrixSDK: 8179c184d819782282f47dab16ce6c2b68ef8a74
|
||||
MatrixSDKCrypto: 7073c382c484cb8ba7dba0a83e112ead96d3bbfd
|
||||
OLMKit: da115f16582e47626616874e20f7bb92222c7a51
|
||||
PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d
|
||||
ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d
|
||||
@@ -204,6 +208,6 @@ SPEC CHECKSUMS:
|
||||
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: d0f3ced275c85b9eadb07bd1b92ad8ae7a40e243
|
||||
PODFILE CHECKSUM: 54848168ab5303c9126626395886cd85f27a44b3
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
Vendored
+3
@@ -8,5 +8,8 @@
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
||||
Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/Contents.json
Vendored
+3
@@ -8,5 +8,8 @@
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "pill_user.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "pill_user@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "pill_user@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 754 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -348,6 +348,8 @@
|
||||
"pin_protection_settings_section_header_x" = "PIN";
|
||||
"pin_protection_choose_pin_welcome_after_login" = "Willkommen!";
|
||||
"pin_protection_choose_pin_welcome_after_register" = "Willkommen!";
|
||||
"pin_protection_reset_button_accessibility_label" = "Löschen";
|
||||
"pin_protection_reset_button_accessibility_hint" = "Löschen";
|
||||
|
||||
// MARK: - Biometrics Protection
|
||||
|
||||
@@ -566,6 +568,7 @@
|
||||
"authentication_server_selection_server_url" = "Server-URL";
|
||||
"authentication_server_selection_server_denied_title" = "Unbekannte URL";
|
||||
"authentication_server_selection_server_denied_message" = "Dein Server ist leider noch nicht für den BundesMessenger eingerichtet. Wenn Du aus der Öffentlichen Verwaltung bist und Fragen hast, wie du den BundesMessenger nutzen kannst, besuche unsere Webseite.";
|
||||
"authentication_server_selection_server_denied_advertizement_website_button" = "Website";
|
||||
|
||||
// MARK: - Netiquette Menu
|
||||
|
||||
@@ -606,3 +609,10 @@
|
||||
"show_my_qr_screen_title" = "Mein QR Code";
|
||||
"show_my_qr_screen_message" = "Dies ist der QR Code zu deinem Profil.\nLasse den QR Code mit der %@ App scannen, damit andere mit dir in Kontakt treten können.";
|
||||
|
||||
// MARK: - Device Manager
|
||||
"user_session_verified_session_description" = "Du hast deine Sitzung durch Eingabe des Wiederherstellungsschlüssels oder durch die Verifizierung mit einem anderen Gerät bestätigt. Dies bedeutet, dass du alle Schlüssel zum Entschlüsseln deiner Nachrichten hast und anderen bestätigst, dieser Sitzung zu vertrauen.";
|
||||
"user_session_button_view_all" = "Alle anzeigen (%d)";
|
||||
|
||||
// MARK: - Voice Over
|
||||
"textfield_reveal_secret" = "Texteingabe anzeigen";
|
||||
"textfield_hide_secret" = "Texteingabe verbergen";
|
||||
|
||||
@@ -2534,7 +2534,7 @@
|
||||
"user_session_details_title" = "Sitzungsdetails";
|
||||
"user_session_push_notifications_message" = "Wenn aktiviert, wird diese Sitzung Push-Benachrichtigungen erhalten.";
|
||||
"user_session_push_notifications" = "Push-Benachrichtigungen";
|
||||
"user_sessions_view_all_action" = "Alle anzeigen (%1$d)";
|
||||
"user_sessions_view_all_action" = "Alle anzeigen (%d)";
|
||||
"user_sessions_overview_security_recommendations_inactive_info" = "Erwäge, dich aus alten (90 Tage oder mehr), nicht mehr verwendeten Sitzungen abzumelden.";
|
||||
"user_sessions_overview_security_recommendations_inactive_title" = "Inaktive Sitzungen";
|
||||
"user_sessions_overview_security_recommendations_unverified_info" = "Nicht verifizierte Sitzungen verifizieren oder abmelden.";
|
||||
@@ -2661,12 +2661,6 @@
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "Du hast nicht verifizierte Sitzungen";
|
||||
"launch_loading_processing_response" = "Verarbeite Daten\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Synchronisiere mit dem Server\n(%@ Versuch)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Synchronisiere mit dem Server";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Diese Sitzung unterstützt keine Verschlüsselung und kann deshalb nicht verifiziert werden.";
|
||||
"voice_broadcast_time_left" = "%@ übrig";
|
||||
"voice_broadcast_buffering" = "Puffere …";
|
||||
@@ -2716,10 +2710,6 @@
|
||||
"wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten";
|
||||
"voice_broadcast_recorder_connection_error" = "Verbindungsfehler − Aufnahme pausiert";
|
||||
"poll_timeline_reply_ended_poll" = "Beendete Umfrage";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Migriere Daten\n%@ %%";
|
||||
"settings_labs_disable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung (zum Deaktivieren abmelden)";
|
||||
"settings_labs_confirm_crypto_sdk" = "Bitte beachte, dass diese Funktion noch experimentell ist, womöglich nicht wie erwartet funktioniert und unerwünschte Nebeneffekte haben kann. Melde dich zum deaktivieren einfach ab und erneut an. Nutze diese Funktion nach eigenem Ermessen und mit Vorsicht.";
|
||||
"settings_labs_enable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung";
|
||||
@@ -2736,3 +2726,19 @@
|
||||
"settings_push_rules_error" = "Ein Fehler ist während der Aktualisierung deiner Benachrichtigungseinstellungen aufgetreten. Bitte versuche die Option erneut umzuschalten.";
|
||||
"poll_history_detail_view_in_timeline" = "Umfrage im Verlauf anzeigen";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Die Verbindung mit diesem Gerät wird nicht unterstützt.";
|
||||
"room_waiting_other_participants_message" = "Sobald eingeladene Benutzer %@ beigetreten sind, werdet ihr euch unterhalten können und der Raum Ende-zu-Ende-verschlüsselt sein";
|
||||
"room_waiting_other_participants_title" = "Warte darauf, dass Benutzer %@ beitreten";
|
||||
"key_verification_scan_qr_code_information_new_session" = "Richte deine Kamera auf den QR-Code deines anderen Gerätes, um deine neue Sitzung zu verifizieren";
|
||||
"key_verification_scan_qr_code_information_other_session" = "Richte deine Kamera auf den QR-Code deines anderen Gerätes, um deine Sitzung zu verifizieren";
|
||||
"key_verification_scan_qr_code_information_other_device" = "Richte deine Kamera auf den QR-Code deines anderen Gerätes, um diese Sitzung zu verifizieren";
|
||||
"key_verification_scan_qr_code_information_other_user" = "Richte deine Kamera auf den QR-Code des anderen Gerätes, um die Sitzung der anderen Person zu verifizieren";
|
||||
"key_verification_scan_qr_code_title" = "QR-Code einlesen";
|
||||
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Du hast keinen Zugriff auf eine bestehende %@-Sitzung?";
|
||||
"device_verification_self_verify_open_on_other_device_information" = "Du musst diese Sitzung verifizieren, um deinen verschlüsselten Nachrichtenverlauf lesen zu können.\n\nÖffne Element auf einem deiner anderen Geräte und folge den Anweisungen.";
|
||||
"device_verification_self_verify_open_on_other_device_title" = "Öffne %@ auf deinem anderen Gerät";
|
||||
"room_creation_only_one_email_invite" = "Du kannst E-Mail-Einladung nur nacheinander verschicken";
|
||||
"launch_loading_delay_warning" = "Das könnte eine Weile dauern.\nDanke für deine Geduld.";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_generic" = "Synchronisiere deine Unterhaltungen";
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
"secure_key_backup_setup_intro_use_security_passphrase_title" = "Recovery Key";
|
||||
"secure_key_backup_setup_intro_use_security_passphrase_info" = "Set up recovery key.";
|
||||
"key_backup_setup_passphrase_confirm_passphrase_title" = "";
|
||||
"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "Recovery Key";
|
||||
"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "Recovery Key";
|
||||
"key_backup_setup_passphrase_confirm_passphrase_invalid" = "doesn’t match";
|
||||
"key_backup_recover_invalid_passphrase_title" = "Incorrect Recovery Key";
|
||||
"key_backup_recover_invalid_passphrase" = "Backup could not be decrypted with this phrase: please verify that you entered the correct recovery key.";
|
||||
@@ -249,15 +249,18 @@
|
||||
"secrets_setup_recovery_key_title" = "Recovery Key";
|
||||
"secrets_setup_recovery_key_information" = "Completed!\n\Your recovery key has been set successfully.";
|
||||
"secrets_setup_recovery_key_done_action" = "Finish";
|
||||
"secrets_setup_recovery_passphrase_title" = "Set a Security Phrase";
|
||||
"secrets_setup_recovery_passphrase_information" = "Enter a security phrase only you know, used to secure secrets on your server.";
|
||||
"secrets_setup_recovery_passphrase_additional_information" = "⚠️ Do not use your password.";
|
||||
"secrets_setup_recovery_passphrase_confirm_information" = "Enter your recovery key again to confirm it.";
|
||||
"secrets_setup_recovery_passphrase_confirm_passphrase_title" = "";
|
||||
"secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "Recovery Key";
|
||||
|
||||
|
||||
"secrets_setup_recovery_passphrase_summary_title" = "Save your recovery key";
|
||||
"secrets_setup_recovery_passphrase_summary_information" = "Remember your recovery key. It can be used to unlock your encrypted messages & data.";
|
||||
"secrets_reset_authentication_message" = "Confirm with your password!";
|
||||
"key_backup_setup_passphrase_passphrase_title" = "Enter";
|
||||
"key_backup_setup_passphrase_passphrase_placeholder" = "Enter phrase";
|
||||
"key_backup_setup_passphrase_passphrase_invalid" = "Try adding a word";
|
||||
|
||||
// MARK: - PIN Protection
|
||||
|
||||
@@ -266,6 +269,8 @@
|
||||
"pin_protection_settings_section_header_x" = "PIN";
|
||||
"pin_protection_choose_pin_welcome_after_login" = "Welcome!";
|
||||
"pin_protection_choose_pin_welcome_after_register" = "Welcome!";
|
||||
"pin_protection_reset_button_accessibility_label" = "Delete";
|
||||
"pin_protection_reset_button_accessibility_hint" = "Delete";
|
||||
|
||||
// MARK: - Biometrics Protection
|
||||
|
||||
@@ -293,7 +298,7 @@
|
||||
"splash_screen_first_info" = "Chats with comrades and colleagues";
|
||||
"splash_screen_second_info" = "Secure transfer of media and files";
|
||||
"splash_screen_third_info" = "Always encrypted";
|
||||
"splash_screen_start" = "Let go";
|
||||
"splash_screen_start" = "Start now";
|
||||
|
||||
// MARK: Clients
|
||||
|
||||
@@ -442,22 +447,23 @@
|
||||
"poll_edit_form_poll_type_open" = "Open poll";
|
||||
|
||||
// MARK: - Welcome Experience
|
||||
"welcome_experience_title1" = "Willkommen beim BundesMessenger";
|
||||
"welcome_experience_description1" = "Aus Deutschland, für Deutschland: Der souveräne Messenger für die besonderen Anforderungen des öffentlichen Sektors.";
|
||||
"welcome_experience_title1" = "Welcome to BundesMessenger";
|
||||
"welcome_experience_description1" = "The secure messaging app for the public service in Germany.";
|
||||
|
||||
"welcome_experience_title2" = "Sicher und von überall";
|
||||
"welcome_experience_description2" = "Von überall auf die eigenen Chats zugreifen und nahtlos zwischen Smartphone, Tablet und Computer wechseln. Und: Alle Daten sind Ende-zu-Ende verschlüsselt auf eigenen Servern gespeichert.";
|
||||
"welcome_experience_title2" = "Secure and from anywhere";
|
||||
"welcome_experience_description2" = "Access your chats from anywhere. Whether you’re using the app on your smartphone, tablet and computer, your data is always encrypted end-to-end and stored securely on private servers.";
|
||||
|
||||
"welcome_experience_title3" = "Einfach vernetzen";
|
||||
"welcome_experience_description3" = "Kolleg*innen aus der gesamten Organisation sofort finden, und zwar ohne erst nach Kontaktdaten suchen zu müssen.";
|
||||
"welcome_experience_title3" = "Connect easily";
|
||||
"welcome_experience_description3" = "Instantly find co-workers from across the organization without having to search for contact information.";
|
||||
|
||||
"welcome_experience_title4" = "Chatten wie gewohnt";
|
||||
"welcome_experience_description4" = "Text, Bilder, Videos, Dokumente, Standorte oder Sprachnachrichten versenden und vieles mehr – an einzelne Personen oder in Gruppen. Und auf Wunsch auch über die Organisationsgrenze hinweg.";
|
||||
"welcome_experience_title4" = "Chat as usual";
|
||||
"welcome_experience_description4" = "Send and receive text and video messages, images, documents, locations, and so much more. Keep in touch via private or group chats with users inside and outside your organization.";
|
||||
|
||||
"welcome_experience_title5" = "Gleich loslegen?";
|
||||
"welcome_experience_description5" = "Ob deine Organisation den BundesMessenger schon eingeführt hat, findest du auf der nächsten Seite heraus. Falls nicht: Überzeuge deinen Admin doch einfach!";
|
||||
"welcome_experience_title5" = "Get started today!";
|
||||
"welcome_experience_description5" = "Next, you can find out whether your organization is already using BundesMessenger. If not, try to convince your boss to become part of the community!";
|
||||
|
||||
"welcome_experience_start" = "Loslegen";
|
||||
|
||||
"welcome_experience_start" = "Start now";
|
||||
|
||||
// MARK: - new login flow
|
||||
"authentication_server_selection_login_title" = "Welcome!";
|
||||
@@ -472,6 +478,7 @@
|
||||
"authentication_server_selection_qr_missing_authorization_message" = "Switch to your iPhone settings app to allow the app to use the camera.";
|
||||
"authentication_server_selection_server_denied_title" = "Invalid URL";
|
||||
"authentication_server_selection_server_denied_message" = "Your server is not configured for the BundesMessenger. If you are part of the Öffentlichen Verwaltung and want more information how to use the BundesMessenger in your organistation then visit our website.";
|
||||
"authentication_server_selection_server_denied_advertizement_website_button" = "Website";
|
||||
|
||||
// MARK: - Netiquette Menu
|
||||
|
||||
@@ -512,3 +519,11 @@
|
||||
"show_my_qr_screen_title" = "My QR Code";
|
||||
"show_my_qr_screen_message" = "This is the QR code to your profile.\nLet others scan the QR code with the %@ app so that they can get in touch with you.";
|
||||
|
||||
// MARK: - Device Manager
|
||||
"user_session_verified_session_description" = "You have confirmed your session by entering the recovery key or verifying with another device. This means that you have all the keys to decrypt your messages and are confirming to others to trust this session.";
|
||||
"user_session_button_view_all" = "View all (%d)";
|
||||
|
||||
// MARK: - Voice Over
|
||||
"textfield_reveal_secret" = "reveal text input";
|
||||
"textfield_hide_secret" = "hide text input";
|
||||
|
||||
|
||||
@@ -815,6 +815,7 @@ Tap the + to start adding people.";
|
||||
"settings_olm_version" = "Olm Version %@";
|
||||
"settings_copyright" = "Copyright";
|
||||
"settings_term_conditions" = "Terms & Conditions";
|
||||
"settings_acceptable_use" = "Acceptable Use Policy";
|
||||
"settings_privacy_policy" = "Privacy Policy";
|
||||
"settings_third_party_notices" = "Third-party Notices";
|
||||
"settings_analytics_and_crash_data" = "Send crash and analytics data";
|
||||
@@ -1560,6 +1561,11 @@ Tap the + to start adding people.";
|
||||
"key_verification_self_verify_current_session_alert_message" = "Other users may not trust it.";
|
||||
"key_verification_self_verify_current_session_alert_validate_action" = "Verify";
|
||||
|
||||
// Legacy to Rust security upgrade
|
||||
|
||||
"key_verification_self_verify_security_upgrade_alert_title" = "App updated";
|
||||
"key_verification_self_verify_security_upgrade_alert_message" = "Secure messaging has been improved with the latest update. Please re-verify your device.";
|
||||
|
||||
// Unverified sessions
|
||||
"key_verification_alert_title" = "You have unverified sessions";
|
||||
"key_verification_alert_body" = "Review to ensure your account is safe.";
|
||||
@@ -1991,10 +1997,8 @@ Tap the + to start adding people.";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Migrating data\n%@ %%";
|
||||
"launch_loading_server_syncing" = "Syncing with the server";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Syncing with the server\n(%@ attempt)";
|
||||
"launch_loading_processing_response" = "Processing data\n%@ %%";
|
||||
"launch_loading_generic" = "Syncing your conversations";
|
||||
"launch_loading_delay_warning" = "This may take a little longer.\nThanks for your patience.";
|
||||
|
||||
// MARK: - Home
|
||||
|
||||
@@ -3158,3 +3162,9 @@ To enable access, tap Settings> Location and select Always";
|
||||
"ssl_unexpected_existing_expl" = "The certificate has changed from one that was trusted by your phone. This is HIGHLY UNUSUAL. It is recommended that you DO NOT ACCEPT this new certificate.";
|
||||
"ssl_expected_existing_expl" = "The certificate has changed from a previously trusted one to one that is not trusted. The server may have renewed its certificate. Contact the server administrator for the expected fingerprint.";
|
||||
"ssl_only_accept" = "ONLY accept the certificate if the server administrator has published a fingerprint that matches the one above.";
|
||||
|
||||
// Pills
|
||||
"pill_room_fallback_display_name" = "Space/Room";
|
||||
"pill_message" = "Message";
|
||||
"pill_message_from" = "Message from %@";
|
||||
"pill_message_in" = "Message in %@";
|
||||
|
||||
@@ -2601,12 +2601,6 @@
|
||||
"key_verification_alert_title" = "Sul on verifitseerimata sessioone";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Seda sessiooni ei saa verifitseerida, sest seal puudub krüptimise tugi.";
|
||||
"voice_broadcast_time_left" = "aega jäänud %@";
|
||||
"launch_loading_processing_response" = "Töötleme andmeid\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Sünkroniseerime andmeid serveriga\n(katse: %@)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Sünkroniseerimine serveriga";
|
||||
"voice_broadcast_buffering" = "Andmed on puhverdamisel…";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Jah, lõpetame";
|
||||
"voice_broadcast_stop_alert_description" = "Kas sa oled kindel, et soovid otseeetri lõpetada? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas.";
|
||||
@@ -2629,7 +2623,7 @@
|
||||
"notice_voice_broadcast_ended" = "%@ lõpetas ringhäälingukõne.";
|
||||
"notice_voice_broadcast_live" = "Ringhäälingukõne on eetris";
|
||||
"user_other_session_security_recommendation_title" = "Muud sessioonid";
|
||||
"poll_timeline_decryption_error" = "Krüptimisvigade tõttu jääb osa hääli lugemata";
|
||||
"poll_timeline_decryption_error" = "Dekrüptimisvigade tõttu jääb osa hääli lugemata";
|
||||
"voice_message_broadcast_in_progress_title" = "Häälsõnumi salvestamine või esitamine ei õnnestu";
|
||||
"voice_message_broadcast_in_progress_message" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne";
|
||||
"poll_timeline_ended_text" = "Küsitlus on lõppenud";
|
||||
@@ -2654,10 +2648,6 @@
|
||||
"wysiwyg_composer_format_action_unordered_list" = "Lülita täpploend sisse/välja";
|
||||
"voice_broadcast_recorder_connection_error" = "Viga võrguühenduses - salvestamine on peatatud";
|
||||
"poll_timeline_reply_ended_poll" = "Lõppenud küsitlus";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Tõstame andmeid ümber\n%@ %%";
|
||||
"settings_labs_disable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine (väljalülitamiseks pead välja logima)";
|
||||
"settings_labs_confirm_crypto_sdk" = "Palun arvesta, et see funktsionaalsus on alles katseline ja ei pruugi toimida eesmärgipäraselt. Kui ta juba on kasutusel, siis väljalülitamiseks pead hiljem korraks võrgust välja logima. Jätka ettevaatlikult ja omal äranägemisel.";
|
||||
"settings_labs_enable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine";
|
||||
@@ -2674,3 +2664,19 @@
|
||||
"settings_push_rules_error" = "Teavituste eelistuste muutmisel tekkis viga. Palun proovi sama valikut uuesti sisse/välja lülitada.";
|
||||
"poll_history_detail_view_in_timeline" = "Näita küsitlust ajajoonel";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Sidumine selle seadmega ei ole toetatud.";
|
||||
"room_waiting_other_participants_message" = "Kui kutse saanud kasutajad on liitunud jututoaga %@, siis saad sa nendega suhelda ja jututuba on läbivalt krüptitud";
|
||||
"room_waiting_other_participants_title" = "Kasutajate liitumise ootel jututoaga %@";
|
||||
"key_verification_scan_qr_code_information_new_session" = "Suuna oma nutiseadme kaamera oma seadmes kuvatavale QR-koodile ja verifitseeri oma uus sessioon";
|
||||
"key_verification_scan_qr_code_information_other_user" = "Suuna oma nutiseadme kaamera teise kasutaja seadmes kuvatavale QR-koodile ja verifitseeri tema sessioon";
|
||||
"key_verification_scan_qr_code_information_other_device" = "Suuna kaamera oma teises seadmes kuvatavale QR-koodile ja verifitseeri see sessioon";
|
||||
"key_verification_scan_qr_code_information_other_session" = "Suuna oma seadme kaamera teises seadmes kuvatavale QR-koodile ja verifitseeri oma sessioon";
|
||||
"key_verification_scan_qr_code_title" = "Loe QR-koodi";
|
||||
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Sul puudub ligipääs olemasolevale %@ sessioonile?";
|
||||
"device_verification_self_verify_open_on_other_device_information" = "Oma krüptitud sõnumite ajaloo lugemiseks pead selle seadme verifitseerima.\n\nAva Element või mõni muu ühilduv Matrixi klient on teises seadmes ja järgi juhendit.";
|
||||
"device_verification_self_verify_open_on_other_device_title" = "Ava %@ oma teises seadmes";
|
||||
"room_creation_only_one_email_invite" = "E-posti teel saad saata kutseid vaid ükshaaval";
|
||||
"launch_loading_delay_warning" = "Selleks võib natuke rohkem aega kuluda.\nTänud ootamast.";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_generic" = "Sinu vestlused on sünkroniseerimisel";
|
||||
|
||||
@@ -2616,7 +2616,6 @@
|
||||
"poll_history_loading_text" = "Afficher les sondages";
|
||||
"voice_message_broadcast_in_progress_title" = "Impossible de démarrer l'enregistrement vocal";
|
||||
"home_context_menu_mark_as_unread" = "Marquer comme non lu";
|
||||
"launch_loading_processing_response" = "Traitement des données\n%@ %%";
|
||||
"notice_voice_broadcast_ended_by_you" = "Vous avez terminé une diffusion vocale.";
|
||||
"notice_voice_broadcast_ended" = "%@ a terminé une diffusion vocale.";
|
||||
"notice_voice_broadcast_live" = "Diffusion en direct";
|
||||
@@ -2721,12 +2720,7 @@
|
||||
// MARK: - Voice Broadcast
|
||||
"voice_broadcast_unauthorized_title" = "Impossible de démarrer une nouvelle diffusion vocale";
|
||||
"voice_message_broadcast_in_progress_message" = "Vous ne pouvez pas démarrer d'enregistrement vocal car vous diffusez en direct. Veuillez interrompre votre diffusion pour démarrer l'enregistrement vocal";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Synchronisation avec le serveur\n(%@ tentatives)";
|
||||
"launch_loading_server_syncing" = "Synchronisation avec le serveur";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Migration des données\n%@ %%";
|
||||
"key_backup_recover_from_private_key_progress" = "%@%% Fini";
|
||||
"room_details_polls" = "Historique des sondages";
|
||||
"settings_labs_disable_crypto_sdk" = "Chiffrement de bout en bout avec Rust (se déconnecter pour désactiver)";
|
||||
|
||||
@@ -2654,12 +2654,7 @@
|
||||
"voice_broadcast_stop_alert_title" = "Megszakítod az élő közvetítést?";
|
||||
"voice_broadcast_buffering" = "Pufferelés…";
|
||||
"voice_broadcast_time_left" = "%@ van vissza";
|
||||
"launch_loading_processing_response" = "Adat feldolgozása\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Szinkronizálás a szerverrel\n(%@ próbálkozás)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Szinkronizálás a szerverrel";
|
||||
"password_policy_pwd_in_dict_error" = "Ez a jelszó megtalálható a szótárban ezért nem engedélyezett.";
|
||||
"password_policy_weak_pwd_error" = "Ez a jelszó túl gyenge. Legalább 8 karakternek kell lennie és minden típusból legalább egy: nagybetű, kisbetű, szám és speciális karakter.";
|
||||
|
||||
@@ -2707,9 +2702,6 @@
|
||||
"poll_history_no_active_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez";
|
||||
"poll_history_loading_text" = "Szavazások megjelenítése";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Adatok migrálása\n%@ %%";
|
||||
"settings_labs_disable_crypto_sdk" = "Rust végpontok közötti titkosítás (kikapcsoláshoz kijelentkezés szükséges)";
|
||||
"settings_labs_confirm_crypto_sdk" = "Ez a funkció még kísérleti fázisban van. Lehet, hogy nem az elvártnak megfelelően fog működni és előre nem látható következménye lehet. A funkció kikapcsolásához egyszerű ki-, és bejelentkezés szükséges. Használata csak saját felelősségre.";
|
||||
"settings_labs_enable_crypto_sdk" = "Rust végpontok közötti titkosítás";
|
||||
@@ -2722,3 +2714,9 @@
|
||||
"poll_history_detail_view_in_timeline" = "Szavazás megjelenítése az idővonalon";
|
||||
"settings_push_rules_error" = "Hiba történt az értesítések beállításának frissítésekor. Próbáld meg az beállítást újra átkapcsolni.";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Ezzel az eszközzel való összeköttetés nem támogatott.";
|
||||
"room_waiting_other_participants_message" = "Miután a meghívott felhasználók csatlakoztak a(z) %@ alkalmazáshoz beszélhet velük és a szoba végpontok között titkosítva lesz";
|
||||
"room_waiting_other_participants_title" = "%@ alkalmazáshoz csatlakozó felhasználókra várakozás";
|
||||
"key_verification_scan_qr_code_title" = "QR kód beolvasása";
|
||||
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Nem férsz hozzá létező munkamenethez, %@?";
|
||||
"device_verification_self_verify_open_on_other_device_title" = "Nyisd meg ezt: %@ a másik eszközön";
|
||||
"room_creation_only_one_email_invite" = "E-mail meghívóból egyszerre csak egy küldhető";
|
||||
|
||||
@@ -2856,12 +2856,6 @@
|
||||
"key_verification_alert_title" = "Anda punya sesi yang belum diverifikasi";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Sesi ini tidak mendukung enkripsi jadi tidak dapat diverifikasi.";
|
||||
"voice_broadcast_time_left" = "Tersisa %@";
|
||||
"launch_loading_processing_response" = "Memroses data\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Menyinkron dengan server\n(%@ percobaan)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Menyinkron dengan server";
|
||||
"voice_broadcast_buffering" = "Memuat…";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Ya, batalkan";
|
||||
"voice_broadcast_stop_alert_description" = "Apakah Anda ingin menghentikan siaran langsung Anda? Ini akan mengakhiri siarannya, dan rekamanan lengkap akan tersedia dalam ruangan.";
|
||||
@@ -2909,10 +2903,6 @@
|
||||
"voice_broadcast_connection_error_title" = "Kesalahan koneksi";
|
||||
"voice_broadcast_recorder_connection_error" = "Kesalahan koneksi - Perekaman dijeda";
|
||||
"poll_timeline_reply_ended_poll" = "Pemungutan suara berakhir";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Memigrasikan data\n%@ %%";
|
||||
"settings_labs_disable_crypto_sdk" = "Enkripsi ujung ke ujung Rust (keluar dari akun untuk menonaktifkan)";
|
||||
"settings_labs_confirm_crypto_sdk" = "Ketahui bahwa fitur ini masih dalam masa eksperimental, ini mungkin tidak berfungsi seperti yang diharapkan dan dapat memiliki konsekuensi yang tidak terduga. Untuk mengembalikan fitur, cukup keluar dari akun dan masuk kembali ke akun. Gunakan dengan pengetahuan dan risiko Anda.";
|
||||
"settings_labs_enable_crypto_sdk" = "Enkripsi ujung ke ujung Rust";
|
||||
@@ -2929,3 +2919,19 @@
|
||||
"poll_history_detail_view_in_timeline" = "Tampilkan pemungutan suara dalam lini masa";
|
||||
"settings_push_rules_error" = "Sebuah kesalahan terjadi ketika memperbarui preferensi notifikasi Anda. Silakan alih ulang opsi Anda.";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Penautan dengan perangkat ini tidak didukung.";
|
||||
"room_waiting_other_participants_message" = "Setelah pengguna yang diundang telah bergabung dengan %@, Anda akan dapat mengobrol dan ruangannya akan terenkripsi secara ujung ke ujung";
|
||||
"room_waiting_other_participants_title" = "Menunggu pengguna untuk bergabung dengan %@";
|
||||
"launch_loading_delay_warning" = "Ini mungkin membutuhkan waktu yang lebih lama.\nTerima kasih atas kesabaran Anda.";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_generic" = "Menyinkronkan percakapan Anda";
|
||||
"key_verification_scan_qr_code_information_new_session" = "Arahkan kamera Anda ke kode QR yang ditampilkan di perangkat Anda yang lain untuk memverifikasi sesi Anda yang baru";
|
||||
"key_verification_scan_qr_code_information_other_session" = "Arahkan kamera Anda ke kode QR yang ditampilkan di perangkat Anda yang lain untuk memverifikasi sesi Anda";
|
||||
"key_verification_scan_qr_code_information_other_device" = "Arahkan kamera Anda ke kode QR yang ditampilkan di perangkat Anda yang lain untuk memverifikasi sesi ini";
|
||||
"key_verification_scan_qr_code_information_other_user" = "Arahkan kamera Anda ke kode QR yang ditampilkan di perangkatnya untuk memverifikasi sesi";
|
||||
"key_verification_scan_qr_code_title" = "Pindai kode QR";
|
||||
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Tidak dapat mengakses sesi %@ yang sudah ada?";
|
||||
"device_verification_self_verify_open_on_other_device_information" = "Anda harus memverifikasi sesi ini supaya dapat membaca riwayat pesan aman Anda.\n\nBuka Element di salah satu perangkat Anda yang lain dan ikuti petunjuknya.";
|
||||
"device_verification_self_verify_open_on_other_device_title" = "Buka %@ di perangkat Anda yang lain";
|
||||
"room_creation_only_one_email_invite" = "Amda hanya dapat mengundang satu surel satu-satu";
|
||||
|
||||
@@ -2630,12 +2630,6 @@
|
||||
"user_other_session_permanently_unverified_additional_info" = "Questa sessione non supporta la crittografia, perciò non può essere verificata.";
|
||||
"voice_broadcast_buffering" = "Buffer...";
|
||||
"voice_broadcast_time_left" = "%@ rimasti";
|
||||
"launch_loading_processing_response" = "Elaborazione dati\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Sincronizzazione con il server\n(%@ tentativo)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Sincronizzazione con il server";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Sì, ferma";
|
||||
"voice_broadcast_stop_alert_description" = "Vuoi davvero fermare la tua trasmissione in diretta? Verrà terminata la trasmissione e la registrazione completa sarà disponibile nella stanza.";
|
||||
"voice_broadcast_stop_alert_title" = "Fermare la trasmissione in diretta?";
|
||||
@@ -2686,10 +2680,6 @@
|
||||
"poll_history_no_past_poll_period_text" = "Non ci sono sondaggi passati negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti";
|
||||
"poll_history_no_active_poll_period_text" = "Non ci sono sondaggi attivi negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti";
|
||||
"poll_history_loading_text" = "Visualizzazione sondaggi";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Migrazione dati\n%@ %%";
|
||||
"settings_labs_disable_crypto_sdk" = "Crittografia end-to-end Rust (disconnettiti per disattivarla)";
|
||||
"settings_labs_confirm_crypto_sdk" = "Si noti che questa funzione, essendo ancora in fase sperimentale, potrebbe non funzionare come previsto e potrebbe avere conseguenze indesiderate. Per disattivare la funzione, è sufficiente disconnettersi e riaccedere. Utilizzare a propria discrezione e con cautela.";
|
||||
"settings_labs_enable_crypto_sdk" = "Crittografia end-to-end Rust";
|
||||
@@ -2702,3 +2692,19 @@
|
||||
"poll_history_detail_view_in_timeline" = "Vedi sondaggio nella linea temporale";
|
||||
"settings_push_rules_error" = "Si è verificato un errore aggiornando le tue preferenze di notifica. Prova ad attivare/disattivare di nuovo l'opzione.";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Il collegamento con questo dispositivo non è supportato.";
|
||||
"room_waiting_other_participants_message" = "Una volta che gli utenti si saranno uniti a %@, potrete scrivervi e la stanza sarà crittografata end-to-end";
|
||||
"room_waiting_other_participants_title" = "In attesa che gli utenti si uniscano a %@";
|
||||
"key_verification_scan_qr_code_information_new_session" = "Punta la fotocamera verso il codice QR mostrato sull'altro tuo dispositivo per verificare la tua nuova sessione";
|
||||
"key_verification_scan_qr_code_information_other_session" = "Punta la fotocamera verso il codice QR mostrato sull'altro tuo dispositivo per verificare la tua sessione";
|
||||
"key_verification_scan_qr_code_information_other_device" = "Punta la fotocamera verso il codice QR mostrato sull'altro tuo dispositivo per verificare questa sessione";
|
||||
"key_verification_scan_qr_code_information_other_user" = "Punta la fotocamera verso il codice QR mostrato sul suo dispositivo per verificare sua la sessione";
|
||||
"key_verification_scan_qr_code_title" = "Scansiona codice QR";
|
||||
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Non riesci ad accedere a una sessione esistente di %@?";
|
||||
"device_verification_self_verify_open_on_other_device_information" = "Devi verificare questa sessione per potere leggere la cronologia dei messaggi sicuri.\n\nApri Element su uno degli altri tuoi dispositivi e segui le istruzioni.";
|
||||
"device_verification_self_verify_open_on_other_device_title" = "Apri %@ sull'altro tuo dispositivo";
|
||||
"room_creation_only_one_email_invite" = "Puoi invitare una sola email alla volta";
|
||||
"launch_loading_delay_warning" = "Potrebbe volerci un po' più tempo.\nGrazie per la pazienza.";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_generic" = "Sincronizzazione delle tue conversazioni";
|
||||
|
||||
@@ -2591,8 +2591,6 @@
|
||||
"user_session_unverified_session_description" = "未認証のセッションは、認証情報でログインされていますが、クロス認証は行われていないセッションです。\n\nこれらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。";
|
||||
"user_session_verified_session_description" = "認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。\n\n認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。";
|
||||
"user_session_push_notifications_message" = "有効にすると、このセッションはプッシュ通知を受信します。";
|
||||
"launch_loading_server_syncing" = "サーバーと同期しています";
|
||||
"launch_loading_processing_response" = "データを処理しています\n%@ %%";
|
||||
"wysiwyg_composer_format_action_link" = "リンクの装飾を適用";
|
||||
"wysiwyg_composer_format_action_inline_code" = "インラインコードの装飾を適用";
|
||||
"wysiwyg_composer_format_action_unordered_list" = "箇条書きリストの表示を切り替える";
|
||||
@@ -2603,9 +2601,6 @@
|
||||
"settings_labs_enable_crypto_sdk" = "Rust エンドツーエンド暗号化";
|
||||
"settings_labs_disable_crypto_sdk" = "Rust エンドツーエンド暗号化(無効にするにはログアウトしてください)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "データを移行しています\n%@ %%";
|
||||
"poll_history_load_more" = "他のアンケートを読み込む";
|
||||
"key_backup_recover_from_private_key_progress" = "%@%%完了";
|
||||
"voice_broadcast_playback_unable_to_decrypt" = "この音声配信を復号化できません。";
|
||||
@@ -2624,7 +2619,6 @@
|
||||
"analytics_prompt_title" = "%@の改善を手伝う";
|
||||
"event_formatter_call_active_video" = "実施中のビデオ通話";
|
||||
"event_formatter_call_active_voice" = "実施中の音声通話";
|
||||
"launch_loading_server_syncing_nth_attempt" = "サーバーと同期しています\n(%@回試行)";
|
||||
"create_room_suggest_room_footer" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。";
|
||||
"create_room_section_footer_type_public" = "スペースの名前だけでなく、招待された人だけが検索し、参加できます。";
|
||||
"searchable_directory_x_network" = "%@ネットワーク";
|
||||
|
||||
@@ -26,6 +26,18 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p>
|
||||
<b>Version 2.6.0</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>Neue Funktionen</b>
|
||||
<ul>
|
||||
<li/>In den Einstellungen unter Sitzungen findest du nun eine Übersicht aller deiner Geräte, die du selbst verwalten kannst.
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<b>Version 2.5.0</b>
|
||||
|
||||
@@ -2572,3 +2572,8 @@
|
||||
// User sessions management
|
||||
"user_sessions_settings" = "Zarządzaj sesjami";
|
||||
"invite_to" = "Zaproś do %@";
|
||||
"authentication_qr_login_start_step1" = "Otwórz Element na innym urządzeniu";
|
||||
"authentication_qr_login_start_subtitle" = "Użyj aparatu tego urządzenia, aby zeskanować kod QR widoczny na innym urządzeniu:";
|
||||
"authentication_qr_login_start_title" = "Zeskanuj kod QR";
|
||||
"authentication_login_with_qr" = "Zaloguj się za pomocą kodu QR";
|
||||
"accessibility_selected" = "wybrane";
|
||||
|
||||
@@ -2626,12 +2626,7 @@
|
||||
"user_session_got_it" = "Entendido";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Esta sessão não suporta encriptação e assim não pode ser verificada.";
|
||||
"voice_broadcast_time_left" = "%@ restando";
|
||||
"launch_loading_processing_response" = "Processando dados\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Sincando com o servidor\n(%@ tentativa)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Sincando com o servidor";
|
||||
"key_verification_alert_body" = "Revise para assegurar que sua conta está segura.";
|
||||
|
||||
// Unverified sessions
|
||||
|
||||
@@ -118,3 +118,6 @@
|
||||
|
||||
/* New file message from a specific person, not referencing a room. */
|
||||
"LOCATION_FROM_USER" = "%@ поделились своим местоположением";
|
||||
|
||||
/* New voice broadcast from a specific person, not referencing a room. */
|
||||
"VOICE_BROADCAST_FROM_USER" = "%@ начал голосовую трансляцию";
|
||||
|
||||
@@ -2200,3 +2200,27 @@
|
||||
// MARK: Password policy errors
|
||||
"password_policy_too_short_pwd_error" = "Очень короткий пароль";
|
||||
"settings_enable_room_message_bubbles" = "Сообщения пузырями";
|
||||
"notice_voice_broadcast_ended" = "%@ закончил(а) голосовую трансляцию.";
|
||||
"notice_voice_broadcast_ended_by_you" = "Вы закончили голосовую трансляцию.";
|
||||
"room_displayname_more_than_two_members" = "%@ и %@ другие";
|
||||
"room_details_access_row_title" = "Доступ";
|
||||
"room_details_polls" = "История опроса";
|
||||
// User sessions management
|
||||
"user_sessions_settings" = "Управление сеансами";
|
||||
"manage_session_sign_out_other_sessions" = "Выйти из всех остальных сеансов";
|
||||
"manage_session_rename" = "Переименовать сеанс";
|
||||
"settings_presence_offline_mode_description" = "Если включено, вы будете всегда оффлайн для остальных пользователей, даже при использовании приложения.";
|
||||
"settings_presence" = "Присутствие";
|
||||
"settings_discovery_accept_terms" = "Примите Правила Идентификации Сервера";
|
||||
"settings_labs_enable_voice_broadcast" = "Голосовая трансляция";
|
||||
"settings_labs_enable_wysiwyg_composer" = "Попробуйте редактор текста";
|
||||
"settings_labs_enable_new_app_layout" = "Новый Слой Приложения";
|
||||
"settings_labs_enable_new_session_manager" = "Новый менеджер сессии";
|
||||
"settings_labs_enable_auto_report_decryption_errors" = "Авто Отчет Ошибок Расшифровки";
|
||||
"settings_push_rules_error" = "Произошла ошибка при обновлении настроек уведомлений. Пожалуйста, попробуйте переключить свой вариант еще раз.";
|
||||
"threads_beta_information" = "Держите обсуждения организованными с помощью потоков.\n\nПотоки помогают вести ваши разговоры по теме и их легко отслеживать. ";
|
||||
"room_creation_only_one_email_invite" = "Вы можете пригласить только один адрес email за раз";
|
||||
"threads_notice_title" = "Потоки больше не экспериментальная функция 🎉";
|
||||
"threads_notice_information" = "Все потоки созданные во время экспериментального периода теперь <b>отображаются как обычные ответы</b>.<br/><br/>Это разовый переход, так как потоки теперь часть спецификации Matrix.";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Связь с этим устройством не поддерживается.";
|
||||
"accessibility_selected" = "выбранный";
|
||||
|
||||
@@ -2852,12 +2852,6 @@
|
||||
"key_verification_alert_title" = "Máte neoverené relácie";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Táto relácia nepodporuje šifrovanie, a preto ju nemožno overiť.";
|
||||
"voice_broadcast_time_left" = "%@ ostáva";
|
||||
"launch_loading_processing_response" = "Spracovanie údajov\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Synchronizácia so serverom\n(%@ pokus)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Synchronizácia so serverom";
|
||||
"voice_broadcast_buffering" = "Načítavanie do vyrovnávacej pamäte…";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Áno, zastaviť";
|
||||
"voice_broadcast_stop_alert_description" = "Určite chcete zastaviť vysielanie naživo? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam.";
|
||||
@@ -2905,10 +2899,6 @@
|
||||
"wysiwyg_composer_format_action_unordered_list" = "Prepnúť zoznam s odrážkami";
|
||||
"voice_broadcast_recorder_connection_error" = "Chyba pripojenia - nahrávanie pozastavené";
|
||||
"poll_timeline_reply_ended_poll" = "Ukončená anketa";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Migrácia údajov\n%@ %%";
|
||||
"settings_labs_disable_crypto_sdk" = "Rust end-to-end šifrovanie (odhláste sa, aby ste ho vypli)";
|
||||
"settings_labs_confirm_crypto_sdk" = "Upozorňujeme, že táto funkcia je stále v experimentálnej fáze, preto nemusí fungovať podľa očakávaní a môže mať potenciálne nezamýšľané dôsledky. Ak chcete funkciu vrátiť späť, jednoducho sa odhláste a znova prihláste. Používajte ju podľa vlastného uváženia a s opatrnosťou.";
|
||||
"settings_labs_enable_crypto_sdk" = "Rust end-to-end šifrovanie";
|
||||
@@ -2925,3 +2915,19 @@
|
||||
"poll_history_detail_view_in_timeline" = "Zobraziť anketu na časovej osi";
|
||||
"settings_push_rules_error" = "Pri aktualizácii vašich predvolieb oznámení došlo k chybe. Skúste prosím prepnúť možnosť znova.";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Prepojenie s týmto zariadením nie je podporované.";
|
||||
"room_waiting_other_participants_message" = "Keď sa pozvaní používatelia pripoja k aplikácii %@, budete môcť konverzovať a miestnosť bude end-to-end šifrovaná";
|
||||
"room_waiting_other_participants_title" = "Čaká sa na používateľov, kým sa pripoja k aplikácii %@";
|
||||
"key_verification_scan_qr_code_information_new_session" = "Nasmerujte kameru na QR kód zobrazený na vašom druhom zariadení a overte vašu novú reláciu";
|
||||
"key_verification_scan_qr_code_information_other_session" = "Nasmerujte kameru na QR kód zobrazený na vašom druhom zariadení a overte vašu reláciu";
|
||||
"key_verification_scan_qr_code_information_other_device" = "Nasmerujte kameru na QR kód zobrazený na vašom druhom zariadení a overte túto reláciu";
|
||||
"key_verification_scan_qr_code_information_other_user" = "Nasmerujte kameru na QR kód zobrazený na ich zariadení a overte ich reláciu";
|
||||
"key_verification_scan_qr_code_title" = "Skenovať QR kód";
|
||||
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Nemôžete získať prístup k existujúcej relácii %@?";
|
||||
"device_verification_self_verify_open_on_other_device_information" = "Ak si chcete prečítať históriu zabezpečených správ, musíte túto reláciu overiť.\n\nOtvorte aplikáciu Element na jednom z vašich iných zariadení a postupujte podľa pokynov.";
|
||||
"device_verification_self_verify_open_on_other_device_title" = "Otvorte %@ na vašom druhom zariadení";
|
||||
"room_creation_only_one_email_invite" = "Naraz môžete pozvať len jeden e-mail";
|
||||
"launch_loading_delay_warning" = "Môže to trvať trochu dlhšie.\nĎakujeme za vašu trpezlivosť.";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_generic" = "Synchronizácia vašich konverzácií";
|
||||
|
||||
@@ -2625,7 +2625,6 @@
|
||||
"user_session_inactive_session_description" = "Sesione jo aktive janë sesione që keni ca kohë që s’i keni përdorur, por që vazhdojnë të marrin kyçe fshehtëzimi.\n\nHeqja e sesioneve jo aktive përmirëson sigurinë dhe punimin dhe e bëjnë të lehtë për ju të identifikoni, nëse një sesion i ri është i dyshimtë.";
|
||||
"user_session_unverified_session_description" = "Sesione të paverifikuar janë sesione ku keni bërë hyrjen me kredencialet tuaja, por që nuk janë ndër-verifikuar.\n\nDuhet ta bëni veçanërisht të qartë se i njihni këto sesione, ngaqë mund të përfaqësojnë përdorim të paautorizuar të llogarisë tuaj.";
|
||||
"user_session_verified_session_description" = "Sesione të verifikuar janë ata kudo që përdorni Element-in pasi të keni dhënë frazëkalimin tuaj, ose pasi të keni ripohuar identitetin tuaj përmes një tjetër sesioni të verifikuar.\n\nKjo do të thotë se zotëroni krejt kyçet e nevojshëm për të shkyçur mesazhet tuaj të fshehtëzuar dhe ripohuar përdoruesve të tjerë se e besoni këtë sesion.";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Po njëkohësohet me shërbyesin\n(Përpjekja e %@)";
|
||||
"user_session_rename_session_title" = "Riemërtim sesionesh";
|
||||
"user_session_inactive_session_title" = "Sesione jo aktive";
|
||||
"user_session_unverified_session_title" = "Sesione të paverifikuar";
|
||||
@@ -2636,11 +2635,6 @@
|
||||
"user_sessions_show_location_info" = "Shfaq adresë IP";
|
||||
"voice_broadcast_time_left" = "Edhe %@";
|
||||
"voice_broadcast_tile" = "Transmetim zanor";
|
||||
"launch_loading_processing_response" = "Po përpunohen të dhëna\n%@ %%";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Po njëkohësohet me shërbyesin";
|
||||
"key_verification_alert_body" = "Shqyrtojini, që të siguroheni se llogaria juaj është e parrezik.";
|
||||
|
||||
// Unverified sessions
|
||||
@@ -2698,10 +2692,6 @@
|
||||
"voice_broadcast_connection_error_message" = "Mjerisht, s’jemi në gjendje të nisim një incizim mu tani. Ju lutemi, riprovoni më vonë.";
|
||||
"voice_broadcast_connection_error_title" = "Gabim lidhjeje";
|
||||
"home_context_menu_mark_as_unread" = "Vëri shenjë si i palexuar";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Po migrohen të dhëna\n%@ %%";
|
||||
"key_backup_recover_from_private_key_progress" = "Plotësuar %@%%";
|
||||
"room_details_polls" = "Historik pyetësorësh";
|
||||
"settings_labs_disable_crypto_sdk" = "Fshehtëzim skaj-më-skaj bazuar në Rust (që ta çaktivizoni, dilni)";
|
||||
@@ -2712,3 +2702,19 @@
|
||||
"wysiwyg_composer_format_action_indent" = "Rrit shmangie kryeradhe";
|
||||
"poll_history_detail_view_in_timeline" = "Shiheni pyetësorin në rrjedhë kohore";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Nuk mbulohet lidhja me këtë pajisje.";
|
||||
"room_waiting_other_participants_message" = "Pasi përdoruesit e ftuar të kenë hyrë në %@, do të jeni në gjendje të bisedoni dhe dhoma do të jetë e fshehtëzuar skaj-më-skaj";
|
||||
"room_waiting_other_participants_title" = "Po pritet që përdoruesit të hyjnë në %@";
|
||||
"launch_loading_delay_warning" = "Kjo mund të zgjasë pak.\nFaleminderit për durimin.";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_generic" = "Po njëkohësohen bisedat tuaja";
|
||||
"key_verification_scan_qr_code_information_new_session" = "Që të verifikoni sesionin tuaj të ri, drejtojeni kamerën tuaj drejt kodit QR të shfaqur në pajisjen tuaj tjetër";
|
||||
"key_verification_scan_qr_code_information_other_session" = "Që të verifikoni sesionin tuaj, drejtojeni kamerën tuaj drejt kodit QR të shfaqur në pajisjen tuaj tjetër";
|
||||
"key_verification_scan_qr_code_information_other_device" = "Që të verifikoni këtë sesion, drejtojeni kamerën tuaj drejt kodit QR të shfaqur në pajisjen tuaj tjetër";
|
||||
"key_verification_scan_qr_code_information_other_user" = "Që të verifikoni sesionin e tij, drejtojeni kamerën tuaj drejt kodit QR të shfaqur në pajisjen e tjetrit";
|
||||
"key_verification_scan_qr_code_title" = "Skanoni kodin QR";
|
||||
"device_verification_self_verify_wait_recover_secrets_additional_help" = "S’hyni dot te një sesion %@ ekzistues?";
|
||||
"device_verification_self_verify_open_on_other_device_information" = "Lypset të verifikoni këtë sesion, që të mund të lexoni historikun e mesazheve tuaj të siguruar.\n\nHapeni Element-in në një nga pajisjet tuaja të tjera dhe ndiqni udhëzimet.";
|
||||
"device_verification_self_verify_open_on_other_device_title" = "Hapeni %@ në pajisjen tuaj tjetër";
|
||||
"room_creation_only_one_email_invite" = "Mund të ftoni vetëm një email në herë";
|
||||
|
||||
@@ -2409,12 +2409,7 @@
|
||||
"spaces_explore_rooms_format" = "Utforska %@";
|
||||
"spaces_create_subspace_title" = "Skapa ett underutrymme";
|
||||
"spaces_add_subspace_title" = "Skapa utrymme inuti %@";
|
||||
"launch_loading_processing_response" = "Hanterar data\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Synkar med servern\n(%@ försök)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Synkar med servern";
|
||||
"key_verification_alert_body" = "Granska för att försäkra att ditt konto är säkert.";
|
||||
|
||||
// Unverified sessions
|
||||
@@ -2653,9 +2648,6 @@
|
||||
"voice_broadcast_connection_error_title" = "Anslutningsfel";
|
||||
"voice_broadcast_playback_lock_screen_placeholder" = "Röstsändning";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Migrerar data\n%@ %%";
|
||||
"room_details_polls" = "Omröstningshistorik";
|
||||
"settings_labs_disable_crypto_sdk" = "Totalsträckskryptering i Rust (logga ut för att stänga av)";
|
||||
"settings_labs_confirm_crypto_sdk" = "Vänligen observera att den här funktionen fortfarande ska anses vara experimentell, den kanske inte fungerar som förväntat eller kan leda till okända konsekvenser. För att återgå, logga ut och logga sedan in igen. Använd på egen risk.";
|
||||
|
||||
@@ -2854,12 +2854,6 @@
|
||||
"key_verification_alert_title" = "У вас є не звірені сеанси";
|
||||
"user_other_session_permanently_unverified_additional_info" = "Цей сеанс не підтримує шифрування, і його не можна звірити.";
|
||||
"voice_broadcast_time_left" = "Залишилося %@";
|
||||
"launch_loading_processing_response" = "Обробка даних\n%@ %%";
|
||||
"launch_loading_server_syncing_nth_attempt" = "Синхронізація з сервером\n(%@ спроба)";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_server_syncing" = "Синхронізація з сервером";
|
||||
"voice_broadcast_buffering" = "Буферизація...";
|
||||
"voice_broadcast_stop_alert_agree_button" = "Так, припинити";
|
||||
"voice_broadcast_stop_alert_description" = "Ви впевнені, що хочете припинити голосову трансляцію? На цьому трансляція завершиться, і повний запис буде доступний у кімнаті.";
|
||||
@@ -2907,10 +2901,6 @@
|
||||
"wysiwyg_composer_format_action_unordered_list" = "Перемкнути на маркований список";
|
||||
"voice_broadcast_recorder_connection_error" = "Помилка з'єднання - Запис призупинено";
|
||||
"poll_timeline_reply_ended_poll" = "Завершене опитування";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_migrating_data" = "Перенесення даних\n%@ %%";
|
||||
"settings_labs_disable_crypto_sdk" = "Наскрізне шифрування Rust (вийдіть, щоб вимкнути)";
|
||||
"settings_labs_confirm_crypto_sdk" = "Зауважте, що оскільки ця функція досі перебуває на стадії експерименту, вона може працювати не так, як очікується, і може мати непередбачувані наслідки. Щоб вимкнути цю функцію, просто вийдіть з системи та увійдіть знову. Використовуйте на власний розсуд і з обережністю.";
|
||||
"settings_labs_enable_crypto_sdk" = "Наскрізне шифрування Rust";
|
||||
@@ -2927,3 +2917,19 @@
|
||||
"settings_push_rules_error" = "Сталася помилка під час оновлення налаштувань сповіщень. Спробуйте змінити налаштування ще раз.";
|
||||
"poll_history_detail_view_in_timeline" = "Переглянути опитування у стрічці";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Пов'язування з цим пристроєм не підтримується.";
|
||||
"room_waiting_other_participants_message" = "Після того, як запрошені користувачі приєднаються до %@, ви зможете спілкуватися з ними, а кімната буде захищена наскрізним шифруванням";
|
||||
"room_waiting_other_participants_title" = "Очікування коли користувачі приєднаються до %@";
|
||||
"key_verification_scan_qr_code_information_new_session" = "Наведіть камеру на QR-код, що показаний на іншому пристрої, щоб звірити сеанс";
|
||||
"key_verification_scan_qr_code_information_other_session" = "Наведіть камеру на QR-код, що показаний на іншому пристрої, щоб звірити сеанс";
|
||||
"key_verification_scan_qr_code_information_other_device" = "Наведіть камеру на QR-код, що показаний на іншому пристрої, щоб звірити сеанс";
|
||||
"key_verification_scan_qr_code_information_other_user" = "Наведіть камеру на QR-код, що показаний на їхньому пристрої, щоб звірити сеанс";
|
||||
"key_verification_scan_qr_code_title" = "Сканувати QR-код";
|
||||
"device_verification_self_verify_wait_recover_secrets_additional_help" = "Не можете отримати доступ до наявного сеансу %@?";
|
||||
"device_verification_self_verify_open_on_other_device_information" = "Вам потрібно звірити цей сеанс, щоб прочитати історію захищених повідомлень.\n\nВідкрийте Element на одному з інших пристроїв і дотримуйтесь інструкцій.";
|
||||
"device_verification_self_verify_open_on_other_device_title" = "Відкрийте %@ на іншому своєму пристрої";
|
||||
"room_creation_only_one_email_invite" = "Ви можете запросити лише одну адресу електронної пошти за раз";
|
||||
"launch_loading_delay_warning" = "Це може тривати трохи довше.\nДякуємо за ваше терпіння.";
|
||||
|
||||
// MARK: - Launch loading
|
||||
|
||||
"launch_loading_generic" = "Синхронізація ваших розмов";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Permissions usage explanations
|
||||
"NSCameraUsageDescription" = "相機權限會用來拍攝照片、影片,與進行視訊通話。";
|
||||
"NSPhotoLibraryUsageDescription" = "允許讀取照片圖庫權限並用來傳送照片與影片。";
|
||||
"NSMicrophoneUsageDescription" = "Element 需要麥克風的權限來進行語音通話、視訊通話與錄製語音訊息。";
|
||||
"NSContactsUsageDescription" = "這將會分享給身份伺服器以便在 Matrix 尋找您的聯絡人。";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "當您分享您的位置給其他人時,Element 需要權限來顯示地圖。";
|
||||
"NSLocationWhenInUseUsageDescription" = "當您分享您的位置給其他人時,Element 需要權限來顯示地圖。";
|
||||
"NSFaceIDUsageDescription" = "已啟用 Face ID 來使用您的應用程式。";
|
||||
"NSCalendarsUsageDescription" = "檢視您已排定的會議。";
|
||||
"NSCameraUsageDescription" = "給予相機權限會用來進行視訊通話或是拍攝並上傳照片與影片。";
|
||||
"NSPhotoLibraryUsageDescription" = "同意使用圖片的權限會用來上傳您圖庫的照片與影片。";
|
||||
"NSMicrophoneUsageDescription" = "Element 需要麥克風的權限來接受通話、拍攝影片以及錄製語音訊息。";
|
||||
"NSContactsUsageDescription" = "他們會與您的身分伺服器共享以找到您在Matrix上的聯絡人。";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "當您與其他人分享您的位置,Element 需要權限將位置顯示在地圖上。";
|
||||
"NSLocationWhenInUseUsageDescription" = "當您與其他人分享您的位置,Element 需要權限將位置顯示在地圖上。";
|
||||
"NSFaceIDUsageDescription" = "您可以使用 Face ID 來登入您的應用程式。";
|
||||
"NSCalendarsUsageDescription" = "在應用程式中查看您已預約的會議。";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* New message from a specific person, not referencing a room */
|
||||
"MSG_FROM_USER" = "%@ 傳來的訊息";
|
||||
"MSG_FROM_USER" = "來自 %@ 的訊息";
|
||||
/* New message from a specific person in a named room */
|
||||
"MSG_FROM_USER_IN_ROOM" = "%@ 在 %@ 貼文";
|
||||
/* New message from a specific person, not referencing a room. Content included. */
|
||||
"MSG_FROM_USER_WITH_CONTENT" = "%@:%@";
|
||||
/* New message from a specific person in a named room. Content included. */
|
||||
"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ 在 %@:%@";
|
||||
"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ 來自 %@:%@";
|
||||
/* New action message from a specific person, not referencing a room. */
|
||||
"ACTION_FROM_USER" = "* %@ %@";
|
||||
/* New action message from a specific person in a named room. */
|
||||
@@ -26,13 +26,13 @@
|
||||
/* Multiple messages in two rooms */
|
||||
"MSGS_IN_TWO_ROOMS" = "%@ 個新訊息來自 %@ 與 %@";
|
||||
/* Look, stuff's happened, alright? Just open the app. */
|
||||
"MSGS_IN_TWO_PLUS_ROOMS" = "%@ 個新訊息來自 %@、%@ 與其他";
|
||||
"MSGS_IN_TWO_PLUS_ROOMS" = "%@ 個新訊息來自 %@、%@ 與其他人";
|
||||
/* A user has invited you to a chat */
|
||||
"USER_INVITE_TO_CHAT" = "%@ 已經邀請您來聊天";
|
||||
"USER_INVITE_TO_CHAT" = "%@ 邀請您來聊天";
|
||||
/* A user has invited you to an (unamed) group chat */
|
||||
"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ 已經邀請您到群組聊天中";
|
||||
"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ 邀請您到群組聊天中";
|
||||
/* A user has invited you to a named room */
|
||||
"USER_INVITE_TO_NAMED_ROOM" = "%@ 已經邀請您加入 %@";
|
||||
"USER_INVITE_TO_NAMED_ROOM" = "%@ 邀請您加入 %@";
|
||||
/* Incoming one-to-one voice call */
|
||||
"VOICE_CALL_FROM_USER" = "來自 %@ 的通話";
|
||||
/* Incoming one-to-one video call */
|
||||
@@ -46,14 +46,14 @@
|
||||
/* Incoming named video conference invite from a specific person */
|
||||
"VIDEO_CONF_NAMED_FROM_USER" = "來自 %@ 的視訊群組通話:'%@'";
|
||||
/* A single unread message in a room */
|
||||
"SINGLE_UNREAD_IN_ROOM" = "您在 %@ 中收到了一則訊息";
|
||||
"SINGLE_UNREAD_IN_ROOM" = "您在 %@ 中收到一則訊息";
|
||||
/* A single unread message */
|
||||
"SINGLE_UNREAD" = "您收到了一則訊息";
|
||||
"SINGLE_UNREAD" = "您收到一則訊息";
|
||||
/* Message title for a specific person in a named room */
|
||||
"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ 從 %@";
|
||||
"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ 來自 %@";
|
||||
|
||||
/* New message reply from a specific person in a named room. */
|
||||
"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ 在 %@ 已回覆";
|
||||
"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ 已從 %@ 回覆";
|
||||
|
||||
/* New message reply from a specific person, not referencing a room. */
|
||||
"REPLY_FROM_USER_TITLE" = "%@ 已回覆";
|
||||
@@ -63,41 +63,41 @@
|
||||
|
||||
/** Key verification **/
|
||||
|
||||
"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ 請求驗證";
|
||||
"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ 希望驗證";
|
||||
|
||||
/* Group call from user, CallKit caller name */
|
||||
"GROUP_CALL_FROM_USER" = "%@ (群組通話)";
|
||||
"GROUP_CALL_FROM_USER" = "%@ (群組通話)";
|
||||
|
||||
/* A user added a Jitsi call to a room */
|
||||
"GROUP_CALL_STARTED" = "群組通話開始";
|
||||
"GROUP_CALL_STARTED" = "群組對話已開始";
|
||||
|
||||
/* A user's membership has updated in an unknown way */
|
||||
"USER_MEMBERSHIP_UPDATED" = "%@ 更新了簡介";
|
||||
"USER_MEMBERSHIP_UPDATED" = "%@ 更新了個人資料";
|
||||
|
||||
/* A user has change their name to a new name which we don't know */
|
||||
"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ 變更名稱";
|
||||
"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ 變更了名字";
|
||||
|
||||
/** Membership Updates **/
|
||||
|
||||
/* A user has change their name to a new name */
|
||||
"USER_UPDATED_DISPLAYNAME" = "%@ 變更名稱為 %@";
|
||||
"USER_UPDATED_DISPLAYNAME" = "%@ 把名稱變更為 %@";
|
||||
|
||||
/* A user has change their avatar */
|
||||
"USER_UPDATED_AVATAR" = "%@ 變更頭像";
|
||||
"USER_UPDATED_AVATAR" = "%@ 變更了他們的頭像";
|
||||
|
||||
/* A user has reacted to a message, but the reaction content is unknown */
|
||||
"GENERIC_REACTION_FROM_USER" = "%@ 送出一個反應";
|
||||
"GENERIC_REACTION_FROM_USER" = "%@ 送出了一個反應";
|
||||
|
||||
/** Reactions **/
|
||||
|
||||
/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */
|
||||
"REACTION_FROM_USER" = "%@ 覺得 %@";
|
||||
"REACTION_FROM_USER" = "%@ 對 %@ 做出了反應";
|
||||
|
||||
/* New message with hidden content due to PIN enabled */
|
||||
"MESSAGE_PROTECTED" = "新訊息";
|
||||
|
||||
/* New message indicator on a room */
|
||||
"MESSAGE_IN_X" = "在 %@ 的訊息";
|
||||
"MESSAGE_IN_X" = "來自 %@ 的訊息";
|
||||
|
||||
/* New message indicator from a DM */
|
||||
"MESSAGE_FROM_X" = "來自 %@ 的訊息";
|
||||
@@ -108,24 +108,27 @@
|
||||
"MESSAGE" = "訊息";
|
||||
|
||||
/* Sticker from a specific person, not referencing a room. */
|
||||
"STICKER_FROM_USER" = "%@ 戳你一下";
|
||||
"STICKER_FROM_USER" = "%@ 傳了一張貼圖";
|
||||
|
||||
/* New file message from a specific person, not referencing a room. */
|
||||
"LOCATION_FROM_USER" = "%@ 已分享他的位置";
|
||||
"LOCATION_FROM_USER" = "%@ 分享了他們的位置";
|
||||
|
||||
/* New file message from a specific person, not referencing a room. */
|
||||
"FILE_FROM_USER" = "%@ 傳送一個檔案 %@";
|
||||
"FILE_FROM_USER" = "%@ 送出一個檔案 %@";
|
||||
|
||||
/* New voice message from a specific person, not referencing a room. */
|
||||
"VOICE_MESSAGE_FROM_USER" = "%@ 傳送一個語音訊息";
|
||||
"VOICE_MESSAGE_FROM_USER" = "%@ 送出一段語音訊息";
|
||||
|
||||
/* New audio message from a specific person, not referencing a room. */
|
||||
"AUDIO_FROM_USER" = "%@ 傳送一個音訊檔案 %@";
|
||||
"AUDIO_FROM_USER" = "%@ 送出一個語音檔案 %@";
|
||||
|
||||
/* New video message from a specific person, not referencing a room. */
|
||||
"VIDEO_FROM_USER" = "%@ 傳送一個影片";
|
||||
"VIDEO_FROM_USER" = "%@ 送出一段影片";
|
||||
|
||||
/** Media Messages **/
|
||||
|
||||
/* New image message from a specific person, not referencing a room. */
|
||||
"PICTURE_FROM_USER" = "%@ 傳送一張圖片";
|
||||
"PICTURE_FROM_USER" = "%@ 送出一張圖片";
|
||||
|
||||
/* New voice broadcast from a specific person, not referencing a room. */
|
||||
"VOICE_BROADCAST_FROM_USER" = "%@ 開始語音廣播";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,13 @@ extension MXBugReportRestClient {
|
||||
dateFormatter.timeZone = TimeZone(identifier: "UTC")
|
||||
userInfo["utc_time"] = dateFormatter.string(from: currentDate)
|
||||
|
||||
// SDKs
|
||||
userInfo["matrix_sdk_version"] = MatrixSDKVersion
|
||||
userInfo["crypto_module"] = MXSDKOptions.sharedInstance().cryptoModuleId
|
||||
if let crypto = mainAccount?.mxSession?.crypto {
|
||||
userInfo["crypto_module_version"] = crypto.version
|
||||
}
|
||||
|
||||
if let customFields = customFields {
|
||||
// combine userInfo with custom fields overriding with custom where there is a conflict
|
||||
userInfo.merge(customFields) { (_, new) in new }
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixSDKCrypto
|
||||
|
||||
/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status
|
||||
/// of `CryptoSDK`, and which uses feature flags to control rollout availability.
|
||||
@@ -31,21 +32,36 @@ import Foundation
|
||||
@objc static let shared = CryptoSDKFeature()
|
||||
|
||||
var isEnabled: Bool {
|
||||
RiotSettings.shared.enableCryptoSDK
|
||||
BWIBuildSettings.shared.useRustEncryption
|
||||
}
|
||||
|
||||
var needsVerificationUpgrade: Bool {
|
||||
get {
|
||||
return RiotSettings.shared.showVerificationUpgradeAlert
|
||||
}
|
||||
set {
|
||||
RiotSettings.shared.showVerificationUpgradeAlert = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private static let FeatureName = "ios-crypto-sdk"
|
||||
private static let FeatureNameV2 = "ios-crypto-sdk-v2"
|
||||
|
||||
private let remoteFeature: RemoteFeaturesClientProtocol
|
||||
private let localFeature: PhasedRolloutFeature
|
||||
|
||||
init(remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared) {
|
||||
init(
|
||||
remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared,
|
||||
localTargetPercentage: Double = 0.5
|
||||
) {
|
||||
var targetPercentage = 0.0
|
||||
if BWIBuildSettings.shared.useRustEncryption {
|
||||
targetPercentage = 1.0
|
||||
}
|
||||
self.remoteFeature = remoteFeature
|
||||
self.localFeature = PhasedRolloutFeature(
|
||||
name: Self.FeatureName,
|
||||
// Local feature is currently set to 0% target, and all availability is fully controlled
|
||||
// by the remote feature. Once the remote is fully rolled out, target for local feature will
|
||||
// be gradually increased.
|
||||
targetPercentage: 0.0
|
||||
targetPercentage: targetPercentage
|
||||
)
|
||||
}
|
||||
|
||||
@@ -92,6 +108,13 @@ import Foundation
|
||||
}
|
||||
|
||||
private func isFeatureEnabled(userId: String) -> Bool {
|
||||
remoteFeature.isFeatureEnabled(Self.FeatureName) || localFeature.isEnabled(userId: userId)
|
||||
// This feature includes app version with a bug, and thus will not be rolled out to 100% users
|
||||
remoteFeature.isFeatureEnabled(Self.FeatureName)
|
||||
|
||||
// Second version of the remote feature with a bugfix and released eventually to 100% users
|
||||
|| remoteFeature.isFeatureEnabled(Self.FeatureNameV2)
|
||||
|
||||
// Local feature
|
||||
|| localFeature.isEnabled(userId: userId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ public class BWIL10n: NSObject {
|
||||
public static var authenticationServerSelectionScanCodeButtonTitle: String {
|
||||
return BWIL10n.tr("Bwi", "authentication_server_selection_scan_code_button_title")
|
||||
}
|
||||
/// Website
|
||||
public static var authenticationServerSelectionServerDeniedAdvertizementWebsiteButton: String {
|
||||
return BWIL10n.tr("Bwi", "authentication_server_selection_server_denied_advertizement_website_button")
|
||||
}
|
||||
/// Dein Server ist leider noch nicht für den BundesMessenger eingerichtet. Wenn Du aus der Öffentlichen Verwaltung bist und Fragen hast, wie du den BundesMessenger nutzen kannst, besuche unsere Webseite.
|
||||
public static var authenticationServerSelectionServerDeniedMessage: String {
|
||||
return BWIL10n.tr("Bwi", "authentication_server_selection_server_denied_message")
|
||||
@@ -947,6 +951,14 @@ public class BWIL10n: NSObject {
|
||||
public static var pinProtectionConfirmPinToChange: String {
|
||||
return BWIL10n.tr("Bwi", "pin_protection_confirm_pin_to_change")
|
||||
}
|
||||
/// Löschen
|
||||
public static var pinProtectionResetButtonAccessibilityHint: String {
|
||||
return BWIL10n.tr("Bwi", "pin_protection_reset_button_accessibility_hint")
|
||||
}
|
||||
/// Löschen
|
||||
public static var pinProtectionResetButtonAccessibilityLabel: String {
|
||||
return BWIL10n.tr("Bwi", "pin_protection_reset_button_accessibility_label")
|
||||
}
|
||||
/// PIN
|
||||
public static var pinProtectionSettingsSectionHeaderX: String {
|
||||
return BWIL10n.tr("Bwi", "pin_protection_settings_section_header_x")
|
||||
@@ -1639,6 +1651,14 @@ public class BWIL10n: NSObject {
|
||||
public static var `switch`: String {
|
||||
return BWIL10n.tr("Bwi", "switch")
|
||||
}
|
||||
/// Texteingabe verbergen
|
||||
public static var textfieldHideSecret: String {
|
||||
return BWIL10n.tr("Bwi", "textfield_hide_secret")
|
||||
}
|
||||
/// Texteingabe anzeigen
|
||||
public static var textfieldRevealSecret: String {
|
||||
return BWIL10n.tr("Bwi", "textfield_reveal_secret")
|
||||
}
|
||||
/// Favoriten
|
||||
public static var titleFavourites: String {
|
||||
return BWIL10n.tr("Bwi", "title_favourites")
|
||||
@@ -1659,6 +1679,14 @@ public class BWIL10n: NSObject {
|
||||
public static var userAvatarViewAccessibilityLabel: String {
|
||||
return BWIL10n.tr("Bwi", "user_avatar_view_accessibility_label")
|
||||
}
|
||||
/// Alle anzeigen (%d)
|
||||
public static func userSessionButtonViewAll(_ p1: Int) -> String {
|
||||
return BWIL10n.tr("Bwi", "user_session_button_view_all", p1)
|
||||
}
|
||||
/// Du hast deine Sitzung durch Eingabe des Wiederherstellungsschlüssels oder durch die Verifizierung mit einem anderen Gerät bestätigt. Dies bedeutet, dass du alle Schlüssel zum Entschlüsseln deiner Nachrichten hast und anderen bestätigst, dieser Sitzung zu vertrauen.
|
||||
public static var userSessionVerifiedSessionDescription: String {
|
||||
return BWIL10n.tr("Bwi", "user_session_verified_session_description")
|
||||
}
|
||||
/// Ansehen
|
||||
public static var view: String {
|
||||
return BWIL10n.tr("Bwi", "view")
|
||||
|
||||
@@ -269,6 +269,7 @@ internal class Asset: NSObject {
|
||||
internal static let locationPinIconBum = ImageAsset(name: "location_pin_icon_bum")
|
||||
internal static let locationShareIcon = ImageAsset(name: "location_share_icon")
|
||||
internal static let locationUserMarker = ImageAsset(name: "location_user_marker")
|
||||
internal static let pillUser = ImageAsset(name: "pill_user")
|
||||
internal static let pollCheckboxDefault = ImageAsset(name: "poll_checkbox_default")
|
||||
internal static let pollCheckboxSelected = ImageAsset(name: "poll_checkbox_selected")
|
||||
internal static let pollCheckboxSelectedBum = ImageAsset(name: "poll_checkbox_selected_bum")
|
||||
|
||||
@@ -3027,6 +3027,14 @@ public class VectorL10n: NSObject {
|
||||
public static var keyVerificationSelfVerifyCurrentSessionAlertValidateAction: String {
|
||||
return VectorL10n.tr("Vector", "key_verification_self_verify_current_session_alert_validate_action")
|
||||
}
|
||||
/// Secure messaging has been improved with the latest update. Please re-verify your device.
|
||||
public static var keyVerificationSelfVerifySecurityUpgradeAlertMessage: String {
|
||||
return VectorL10n.tr("Vector", "key_verification_self_verify_security_upgrade_alert_message")
|
||||
}
|
||||
/// App updated
|
||||
public static var keyVerificationSelfVerifySecurityUpgradeAlertTitle: String {
|
||||
return VectorL10n.tr("Vector", "key_verification_self_verify_security_upgrade_alert_title")
|
||||
}
|
||||
/// Review
|
||||
public static var keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction: String {
|
||||
return VectorL10n.tr("Vector", "key_verification_self_verify_unverified_sessions_alert_validate_action")
|
||||
@@ -3191,21 +3199,13 @@ public class VectorL10n: NSObject {
|
||||
public static var later: String {
|
||||
return VectorL10n.tr("Vector", "later")
|
||||
}
|
||||
/// Migrating data\n%@ %%
|
||||
public static func launchLoadingMigratingData(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "launch_loading_migrating_data", p1)
|
||||
/// This may take a little longer.\nThanks for your patience.
|
||||
public static var launchLoadingDelayWarning: String {
|
||||
return VectorL10n.tr("Vector", "launch_loading_delay_warning")
|
||||
}
|
||||
/// Processing data\n%@ %%
|
||||
public static func launchLoadingProcessingResponse(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "launch_loading_processing_response", p1)
|
||||
}
|
||||
/// Syncing with the server
|
||||
public static var launchLoadingServerSyncing: String {
|
||||
return VectorL10n.tr("Vector", "launch_loading_server_syncing")
|
||||
}
|
||||
/// Syncing with the server\n(%@ attempt)
|
||||
public static func launchLoadingServerSyncingNthAttempt(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "launch_loading_server_syncing_nth_attempt", p1)
|
||||
/// Syncing your conversations
|
||||
public static var launchLoadingGeneric: String {
|
||||
return VectorL10n.tr("Vector", "launch_loading_generic")
|
||||
}
|
||||
/// Leave
|
||||
public static var leave: String {
|
||||
@@ -4699,6 +4699,22 @@ public class VectorL10n: NSObject {
|
||||
public static func photoLibraryAccessNotGranted(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "photo_library_access_not_granted", p1)
|
||||
}
|
||||
/// Message
|
||||
public static var pillMessage: String {
|
||||
return VectorL10n.tr("Vector", "pill_message")
|
||||
}
|
||||
/// Message from %@
|
||||
public static func pillMessageFrom(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "pill_message_from", p1)
|
||||
}
|
||||
/// Message in %@
|
||||
public static func pillMessageIn(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "pill_message_in", p1)
|
||||
}
|
||||
/// Space/Room
|
||||
public static var pillRoomFallbackDisplayName: String {
|
||||
return VectorL10n.tr("Vector", "pill_room_fallback_display_name")
|
||||
}
|
||||
/// Create a PIN for security
|
||||
public static var pinProtectionChoosePin: String {
|
||||
return VectorL10n.tr("Vector", "pin_protection_choose_pin")
|
||||
@@ -7203,6 +7219,10 @@ public class VectorL10n: NSObject {
|
||||
public static var settingsAbout: String {
|
||||
return VectorL10n.tr("Vector", "settings_about")
|
||||
}
|
||||
/// Acceptable Use Policy
|
||||
public static var settingsAcceptableUse: String {
|
||||
return VectorL10n.tr("Vector", "settings_acceptable_use")
|
||||
}
|
||||
/// Invalid credentials
|
||||
public static var settingsAdd3pidInvalidPasswordMessage: String {
|
||||
return VectorL10n.tr("Vector", "settings_add_3pid_invalid_password_message")
|
||||
|
||||
@@ -225,6 +225,9 @@ final class RiotSettings: NSObject {
|
||||
@UserDefault(key: "hideVerifyThisSessionAlert", defaultValue: false, storage: defaults)
|
||||
var hideVerifyThisSessionAlert
|
||||
|
||||
@UserDefault(key: "showVerificationUpgradeAlert", defaultValue: false, storage: defaults)
|
||||
var showVerificationUpgradeAlert
|
||||
|
||||
@UserDefault(key: "matrixApps", defaultValue: false, storage: defaults)
|
||||
var matrixApps
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ struct SentryMonitoringClient {
|
||||
if let message = event.message?.formatted {
|
||||
event.fingerprint = [message]
|
||||
}
|
||||
event.tags = [
|
||||
"crypto_module": MXSDKOptions.sharedInstance().cryptoModuleId
|
||||
]
|
||||
MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)")
|
||||
return event
|
||||
}
|
||||
|
||||
@@ -467,11 +467,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
|
||||
// Configure our analytics. It will indeed start if the option is enabled
|
||||
Analytics *analytics = [Analytics shared];
|
||||
|
||||
[MXSDKOptions sharedInstance].analyticsDelegate = BWIAnalytics.sharedTracker;
|
||||
[DecryptionFailureTracker sharedInstance].delegate = BWIAnalytics.sharedTracker;
|
||||
|
||||
|
||||
MXBaseProfiler *profiler = [MXBaseProfiler new];
|
||||
profiler.analytics = analytics;
|
||||
[MXSDKOptions sharedInstance].profiler = profiler;
|
||||
@@ -650,6 +646,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
[MXSDKOptions.sharedInstance.profiler resume];
|
||||
|
||||
_isAppForeground = YES;
|
||||
|
||||
// bwi: check if requests need to be blocked
|
||||
[self checkBlockingServerMaintenance];
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application
|
||||
@@ -663,6 +662,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
self.isApplicationActiveFromSystemAlert = NO;
|
||||
|
||||
[self checkCrossSigningForSession:self.mxSessions.firstObject];
|
||||
|
||||
// bwi: check if requests need to be blocked
|
||||
[self checkBlockingServerMaintenance];
|
||||
}
|
||||
|
||||
- (void)configurePinCodeScreenFor:(UIApplication *)application
|
||||
@@ -1903,6 +1905,13 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
{
|
||||
// Configure analytics from the session if necessary
|
||||
[Analytics.shared useAnalyticsSettingsFrom:mxSession];
|
||||
|
||||
// bwi: add session to app config
|
||||
AppConfigService.shared.session = mxSession;
|
||||
[BWIAnalytics.sharedTracker setSessionWithSession:mxSession];
|
||||
|
||||
[MXSDKOptions sharedInstance].analyticsDelegate = BWIAnalytics.sharedTracker;
|
||||
[DecryptionFailureTracker sharedInstance].delegate = BWIAnalytics.sharedTracker;
|
||||
}
|
||||
else if (mxSession.state == MXSessionStateClosed)
|
||||
{
|
||||
@@ -2093,8 +2102,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
// Do the one time check on device id
|
||||
[self checkDeviceId:mxSession];
|
||||
|
||||
[BWIAnalytics.sharedTracker setSessionWithSession:mxSession];
|
||||
|
||||
[self.delegate legacyAppDelegate:self didAddMatrixSession:mxSession];
|
||||
}
|
||||
}
|
||||
@@ -4797,6 +4804,19 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
[service registerForAppConfig];
|
||||
}
|
||||
|
||||
#pragma mark - bwi blocking maintenance
|
||||
|
||||
// bwi: on appstart and app coming back from background check the current maintenance and block requests if necessary
|
||||
- (void) checkBlockingServerMaintenance {
|
||||
ServerDowntimeDefaultService* service = [[ServerDowntimeDefaultService alloc] init];
|
||||
|
||||
if( [service isDowntimeNow] && [service isBlocking] ) {
|
||||
[NSUserDefaults.standardUserDefaults setBool:YES forKey:@"ServerDownTimeBlockingKey"];
|
||||
} else {
|
||||
[NSUserDefaults.standardUserDefaults setBool:NO forKey:@"ServerDownTimeBlockingKey"];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Mandatory SSSS setup
|
||||
|
||||
- (void)presentSecureBackupSetupForSession:(MXSession*)mxSession
|
||||
|
||||
@@ -142,7 +142,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
|
||||
let flow: AuthenticationFlow = initialScreen == .login ? .login : .register
|
||||
|
||||
// bwi: force serverselection on test apps
|
||||
if BWIBuildSettings.shared.authScreenShowTestServerOptions && BWIBuildSettings.shared.bwiLoginFlowLayout {
|
||||
if (BWIBuildSettings.shared.authScreenShowTestServerOptions && BWIBuildSettings.shared.bwiLoginFlowLayout) ||
|
||||
BWIBuildSettings.shared.bumLoginFlowLayout {
|
||||
showServerSelectionScreen(for: flow)
|
||||
} else {
|
||||
do {
|
||||
|
||||
@@ -1196,8 +1196,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou
|
||||
{
|
||||
RecentsInvitesTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[RecentsInvitesTableViewCell defaultReuseIdentifier]];
|
||||
|
||||
// bwi: fixes mismatched number of invites
|
||||
tableViewCell.invitesCount = self.invitesCellDataArray.count;
|
||||
tableViewCell.invitesCount = self.recentsListService.invitedRoomListData.counts.total.numberOfRooms;
|
||||
|
||||
return tableViewCell;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
|
||||
private let userIndicatorStore: UserIndicatorStore
|
||||
private var appStateIndicatorCancel: UserIndicatorCancel?
|
||||
private var appSateIndicator: UserIndicator?
|
||||
|
||||
private var maintenanceTimer: Timer?
|
||||
|
||||
// Indicate if the Coordinator has started once
|
||||
private var hasStartedOnce: Bool {
|
||||
@@ -141,21 +143,11 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
|
||||
}
|
||||
|
||||
func pinUnlocked() {
|
||||
let serverDownTime = ServerDowntimeDefaultService()
|
||||
// bwi: bwi specific
|
||||
if BWIBuildSettings.shared.useTestDataForDowntime {
|
||||
serverDownTime.fetchDowntimes(completion: {
|
||||
self.createLeftButtonItem(for: self.allChatsViewController)
|
||||
})
|
||||
} else {
|
||||
if let session = self.currentMatrixSession {
|
||||
serverDownTime.fetchDowntimes(session: session, completion: {
|
||||
self.createLeftButtonItem(for: self.allChatsViewController)
|
||||
})
|
||||
}
|
||||
}
|
||||
checkMaintenanceStatus()
|
||||
enableMaintenanceTimer(true)
|
||||
|
||||
self.bwiCheckForMatomoPromt()
|
||||
BWIAnalytics.sharedTracker.readUserConfig()
|
||||
}
|
||||
|
||||
func bwiOnUnlockedByPin() {
|
||||
@@ -355,7 +347,9 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
|
||||
private weak var avatarMenuButton: UIButton?
|
||||
|
||||
private func createLeftButtonItem(for viewController: UIViewController) {
|
||||
createAvatarButtonItem(for: viewController)
|
||||
DispatchQueue.main.async {
|
||||
self.createAvatarButtonItem(for: viewController)
|
||||
}
|
||||
}
|
||||
|
||||
private var avatarMenu: UIMenu {
|
||||
@@ -392,7 +386,8 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
|
||||
view.backgroundColor = .clear
|
||||
|
||||
let avatarInsets: UIEdgeInsets = .init(top: 7, left: 7, bottom: 7, right: 7)
|
||||
var button: UIButton = .init(frame: view.bounds.inset(by: avatarInsets))
|
||||
var button: UIButton = .init(frame: view.bounds)
|
||||
button.imageEdgeInsets = avatarInsets
|
||||
button.setImage(Asset.Images.tabPeople.image, for: .normal)
|
||||
|
||||
if BWIBuildSettings.shared.enableSideMenu {
|
||||
@@ -427,12 +422,12 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func updateAvatarButtonItem() {
|
||||
guard let avatarView = avatarMenuView, let button = avatarMenuButton, let avatar = userAvatarViewData(from: currentMatrixSession) else {
|
||||
return
|
||||
if let avatar = userAvatarViewData(from: currentMatrixSession) {
|
||||
avatarMenuView?.fill(with: avatar)
|
||||
avatarMenuButton?.setImage(nil, for: .normal)
|
||||
} else {
|
||||
avatarMenuButton?.setImage(Asset.Images.tabPeople.image, for: .normal)
|
||||
}
|
||||
|
||||
button.setImage(nil, for: .normal)
|
||||
avatarView.fill(with: avatar)
|
||||
}
|
||||
|
||||
private func showRoom(withId roomId: String, eventId: String? = nil) {
|
||||
@@ -705,12 +700,50 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
// bwi: check if matomo promt was shown for this session
|
||||
// bwi: check if matomo promt was shown for this session otherwise check if config has changed
|
||||
private func bwiCheckForMatomoPromt() {
|
||||
if BWIBuildSettings.shared.bwiMatomoEnabled && BWIAnalytics.sharedTracker.needsToShowPromt() {
|
||||
self.allChatsViewController.bwiPresentMatomoConsentAlert()
|
||||
}
|
||||
}
|
||||
|
||||
// bwi: check if there are changes in maintenance status
|
||||
@objc private func checkMaintenanceStatus() {
|
||||
let serverDownTime = ServerDowntimeDefaultService()
|
||||
// bwi: bwi specific
|
||||
if BWIBuildSettings.shared.useTestDataForDowntime {
|
||||
serverDownTime.fetchDowntimes(completion: {
|
||||
if serverDownTime.isChanged() {
|
||||
self.createLeftButtonItem(for: self.allChatsViewController)
|
||||
serverDownTime.saveCurrentStatus()
|
||||
}
|
||||
if serverDownTime.isBlocking() && serverDownTime.isDowntimeNow() {
|
||||
UserDefaults.standard.set(true, forKey: "ServerDownTimeBlockingKey")
|
||||
} else {
|
||||
UserDefaults.standard.set(false, forKey: "ServerDownTimeBlockingKey")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
serverDownTime.fetchDowntimesWithDirectRequest { success in
|
||||
if serverDownTime.isChanged() {
|
||||
self.createLeftButtonItem(for: self.allChatsViewController)
|
||||
serverDownTime.saveCurrentStatus()
|
||||
}
|
||||
if serverDownTime.isBlocking() && serverDownTime.isDowntimeNow() {
|
||||
UserDefaults.standard.set(true, forKey: "ServerDownTimeBlockingKey")
|
||||
} else {
|
||||
UserDefaults.standard.set(false, forKey: "ServerDownTimeBlockingKey")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func enableMaintenanceTimer(_ enable: Bool ) {
|
||||
maintenanceTimer?.invalidate()
|
||||
if enable {
|
||||
maintenanceTimer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(checkMaintenanceStatus), userInfo: nil, repeats: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AllChatsCoordinator: SignOutFlowPresenterDelegate {
|
||||
@@ -733,7 +766,12 @@ extension AllChatsCoordinator: SignOutFlowPresenterDelegate {
|
||||
extension AllChatsCoordinator: AllChatsViewControllerDelegate {
|
||||
func allChatsViewControllerDidCompleteAuthentication(_ allChatsViewController: AllChatsViewController) {
|
||||
self.delegate?.splitViewMasterCoordinatorDidCompleteAuthentication(self)
|
||||
|
||||
checkMaintenanceStatus()
|
||||
enableMaintenanceTimer(true)
|
||||
|
||||
self.bwiCheckForMatomoPromt()
|
||||
BWIAnalytics.sharedTracker.resetMatomo()
|
||||
}
|
||||
|
||||
func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomWithParameters roomNavigationParameters: RoomNavigationParameters, completion: @escaping () -> Void) {
|
||||
|
||||
@@ -64,6 +64,10 @@ class AllChatsViewController: HomeViewController {
|
||||
// bwi: new feature banner height
|
||||
private var featureBannerViewHeight: CGFloat = 0
|
||||
|
||||
// bwi: application privacy modal for matomo consent
|
||||
private var navigationBar: UINavigationController?
|
||||
private var showMatomoConsentAlertOnCloseModal: Bool = false
|
||||
|
||||
private var bannerView: UIView? {
|
||||
didSet {
|
||||
bannerView?.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -297,6 +301,13 @@ class AllChatsViewController: HomeViewController {
|
||||
super.startActivityIndicator()
|
||||
}
|
||||
|
||||
// bwi: show alert again when modal is dismissed (button press)
|
||||
@objc func bwiCloseModal() {
|
||||
showMatomoConsentAlertOnCloseModal = false
|
||||
navigationBar?.dismiss(animated: true)
|
||||
bwiPresentMatomoConsentAlert()
|
||||
}
|
||||
|
||||
func bwiPresentMatomoConsentAlert() {
|
||||
let alert = UIAlertController(title: BWIL10n.bwiAnalyticsAlertTitle,
|
||||
message: BWIL10n.bwiAnalyticsAlertBody(AppInfo.current.displayName),
|
||||
@@ -304,12 +315,16 @@ class AllChatsViewController: HomeViewController {
|
||||
|
||||
alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertInfoButton,
|
||||
style: .default,
|
||||
handler: { action in
|
||||
if let webviewController = WebViewViewController(url: BWIBuildSettings.shared.applicationPrivacyPolicyUrlString) {
|
||||
webviewController.title = VectorL10n.settingsPrivacyPolicy
|
||||
|
||||
self.present(webviewController, animated: true)
|
||||
}
|
||||
handler: { [self] action in
|
||||
if let webViewController = WebViewViewController(url: BWIBuildSettings.shared.applicationPrivacyPolicyWithMatomoSectionUrlString) {
|
||||
navigationBar = UINavigationController(rootViewController: webViewController)
|
||||
webViewController.navigationItem.setLeftBarButton(UIBarButtonItem(title: VectorL10n.close, style: .plain, target: self, action: #selector(self.bwiCloseModal)), animated: false)
|
||||
webViewController.title = VectorL10n.settingsPrivacyPolicy
|
||||
navigationBar?.presentationController?.delegate = self
|
||||
showMatomoConsentAlertOnCloseModal = true
|
||||
present(navigationBar ?? webViewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}))
|
||||
|
||||
alert.addAction(UIAlertAction(title: BWIL10n.bwiAnalyticsAlertCancelButton,
|
||||
@@ -813,6 +828,11 @@ extension AllChatsViewController: UISearchControllerDelegate {
|
||||
extension AllChatsViewController: UIAdaptivePresentationControllerDelegate {
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
// bwi: show matomo consent alert again
|
||||
if showMatomoConsentAlertOnCloseModal {
|
||||
showMatomoConsentAlertOnCloseModal = false
|
||||
bwiPresentMatomoConsentAlert()
|
||||
}
|
||||
guard let coordinator = childCoordinators.last else {
|
||||
return
|
||||
}
|
||||
@@ -1067,8 +1087,20 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol {
|
||||
private func presentVerifyCurrentSessionAlert(with session: MXSession) {
|
||||
MXLog.debug("[AllChatsViewController] presentVerifyCurrentSessionAlertWithSession")
|
||||
|
||||
let alert = UIAlertController(title: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertTitle,
|
||||
message: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertMessage,
|
||||
let title: String
|
||||
let message: String
|
||||
|
||||
if let feature = MXSDKOptions.sharedInstance().cryptoSDKFeature,
|
||||
feature.isEnabled && feature.needsVerificationUpgrade {
|
||||
title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle
|
||||
message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage
|
||||
} else {
|
||||
title = VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertTitle
|
||||
message = VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertMessage
|
||||
}
|
||||
|
||||
let alert = UIAlertController(title: title,
|
||||
message: message,
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertValidateAction,
|
||||
@@ -1121,7 +1153,7 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol {
|
||||
|
||||
settingsViewController.loadViewIfNeeded()
|
||||
AppDelegate.theDelegate().restoreInitialDisplay {
|
||||
if RiotSettings.shared.enableNewSessionManager {
|
||||
if RiotSettings.shared.enableNewSessionManager || BWIBuildSettings.shared.enableNewSessionManagerByDefault {
|
||||
self.navigationController?.viewControllers = [self, settingsViewController]
|
||||
settingsViewController.showUserSessionsFlow()
|
||||
} else {
|
||||
|
||||
@@ -30,6 +30,8 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
|
||||
// MARK: - Properties
|
||||
|
||||
@IBOutlet private weak var animationView: ElementView!
|
||||
@IBOutlet private weak var progressContainer: UIStackView!
|
||||
@IBOutlet private weak var progressView: UIProgressView!
|
||||
@IBOutlet private weak var statusLabel: UILabel!
|
||||
|
||||
private var animationTimeline: Timeline_1!
|
||||
@@ -54,7 +56,7 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
|
||||
animationTimeline.play()
|
||||
self.animationTimeline = animationTimeline
|
||||
|
||||
self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableStartupProgress
|
||||
progressContainer.isHidden = true
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -66,18 +68,18 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
|
||||
}
|
||||
|
||||
extension LaunchLoadingView: MXSessionStartupProgressDelegate {
|
||||
func sessionDidUpdateStartupStage(_ stage: MXSessionStartupStage) {
|
||||
func sessionDidUpdateStartupProgress(state: MXSessionStartupProgress.State) {
|
||||
guard MXSDKOptions.sharedInstance().enableStartupProgress else {
|
||||
return
|
||||
}
|
||||
updateStatusText(for: stage)
|
||||
|
||||
update(with: state)
|
||||
|
||||
}
|
||||
|
||||
private func updateStatusText(for stage: MXSessionStartupStage) {
|
||||
private func update(with state: MXSessionStartupProgress.State) {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.updateStatusText(for: stage)
|
||||
self?.update(with: state)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -85,24 +87,9 @@ extension LaunchLoadingView: MXSessionStartupProgressDelegate {
|
||||
// Sync may be doing a lot of heavy work on the main thread and the status text
|
||||
// does not update reliably enough without explicitly refreshing
|
||||
CATransaction.begin()
|
||||
statusLabel.text = statusText(for: stage)
|
||||
progressContainer.isHidden = false
|
||||
progressView.progress = Float(state.progress)
|
||||
statusLabel.text = state.showDelayWarning ? VectorL10n.launchLoadingDelayWarning : VectorL10n.launchLoadingGeneric
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
private func statusText(for stage: MXSessionStartupStage) -> String {
|
||||
switch stage {
|
||||
case .migratingData(let progress):
|
||||
let percent = Int(floor(progress * 100))
|
||||
return VectorL10n.launchLoadingMigratingData("\(percent)")
|
||||
case .serverSyncing(let attempts):
|
||||
if attempts > 1, let nth = numberFormatter.string(from: NSNumber(value: attempts)) {
|
||||
return VectorL10n.launchLoadingServerSyncingNthAttempt(nth)
|
||||
} else {
|
||||
return VectorL10n.launchLoadingServerSyncing
|
||||
}
|
||||
case .processingResponse(let progress):
|
||||
let percent = Int(floor(progress * 100))
|
||||
return VectorL10n.launchLoadingProcessingResponse("\(percent)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,32 +14,52 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleAspectFit" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3KG-IR-FPV" customClass="ElementView" customModule="Element" customModuleProvider="target">
|
||||
<view contentMode="scaleAspectFit" placeholderIntrinsicWidth="130" placeholderIntrinsicHeight="130" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3KG-IR-FPV" customClass="ElementView" customModule="Element" customModuleProvider="target">
|
||||
<rect key="frame" x="95" y="219" width="130" height="130"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wzS-bN-Pht">
|
||||
<rect key="frame" x="20" y="528" width="280" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" systemColor="systemGrayColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="hme-ss-nFP">
|
||||
<rect key="frame" x="20" y="472" width="280" height="36"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Syncing" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wzS-bN-Pht">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" systemColor="systemGrayColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DEk-Va-xdT">
|
||||
<rect key="frame" x="0.0" y="32" width="280" height="4"/>
|
||||
<color key="tintColor" red="0.050980392156862744" green="0.74117647058823533" blue="0.54509803921568623" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</progressView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="750" constant="350" id="hle-Py-f0J"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="wzS-bN-Pht" secondAttribute="trailing" constant="20" id="Naf-Cc-qLq"/>
|
||||
<constraint firstAttribute="bottom" secondItem="wzS-bN-Pht" secondAttribute="bottom" constant="40" id="cnE-Pn-Wb2"/>
|
||||
<constraint firstAttribute="bottom" secondItem="hme-ss-nFP" secondAttribute="bottom" constant="60" id="Ayr-Nh-R8M"/>
|
||||
<constraint firstItem="hme-ss-nFP" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" id="Ecw-cR-GaE"/>
|
||||
<constraint firstItem="hme-ss-nFP" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="KSs-HF-BlI"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="hme-ss-nFP" secondAttribute="trailing" constant="20" id="U6l-o1-IL3"/>
|
||||
<constraint firstItem="3KG-IR-FPV" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="ig4-YX-FoT"/>
|
||||
<constraint firstItem="3KG-IR-FPV" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="r9K-7c-fjh"/>
|
||||
<constraint firstItem="wzS-bN-Pht" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" id="uZP-JW-dVR"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="animationView" destination="3KG-IR-FPV" id="Are-fn-laY"/>
|
||||
<outlet property="progressContainer" destination="hme-ss-nFP" id="XC1-4m-OD7"/>
|
||||
<outlet property="progressView" destination="DEk-Va-xdT" id="PCI-Vi-Zyg"/>
|
||||
<outlet property="statusLabel" destination="wzS-bN-Pht" id="Mj2-rn-i5x"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="136.875" y="132.5"/>
|
||||
</view>
|
||||
</objects>
|
||||
<designables>
|
||||
<designable name="3KG-IR-FPV">
|
||||
<size key="intrinsicContentSize" width="130" height="130"/>
|
||||
</designable>
|
||||
</designables>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
// Always show a warning badge if there was a decryption error.
|
||||
if (event.decryptionError)
|
||||
{
|
||||
return EventEncryptionDecorationDecryptionError;
|
||||
return EventEncryptionDecorationRed;
|
||||
}
|
||||
|
||||
// Unencrypted message events should show a warning unless they're pending local echoes
|
||||
@@ -199,29 +199,27 @@
|
||||
return EventEncryptionDecorationNone;
|
||||
}
|
||||
|
||||
return EventEncryptionDecorationNotEncrypted;
|
||||
return EventEncryptionDecorationRed;
|
||||
}
|
||||
|
||||
// The encryption is in a good state.
|
||||
// Only show a warning badge if there are trust issues.
|
||||
if (event.sender)
|
||||
// Only show a warning badge if there are decryption trust issues.
|
||||
if (event.decryptionDecoration)
|
||||
{
|
||||
MXUserTrustLevel *userTrustLevel = [session.crypto trustLevelForUser:event.sender];
|
||||
MXDeviceInfo *deviceInfo = [session.crypto eventDeviceInfo:event];
|
||||
|
||||
if (userTrustLevel.isVerified && !deviceInfo.trustLevel.isVerified)
|
||||
switch (event.decryptionDecoration.color)
|
||||
{
|
||||
return EventEncryptionDecorationUntrustedDevice;
|
||||
case MXEventDecryptionDecorationColorNone:
|
||||
return EventEncryptionDecorationNone;
|
||||
case MXEventDecryptionDecorationColorGrey:
|
||||
return EventEncryptionDecorationGrey;
|
||||
case MXEventDecryptionDecorationColorRed:
|
||||
return EventEncryptionDecorationRed;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.isUntrusted)
|
||||
else
|
||||
{
|
||||
return EventEncryptionDecorationUnsafeKey;
|
||||
return EventEncryptionDecorationNone;
|
||||
}
|
||||
|
||||
// Everything was fine
|
||||
return EventEncryptionDecorationNone;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1362,14 +1362,21 @@ static NSString *const kRepliedTextPattern = @"<mx-reply>.*<blockquote>.*<br>(.*
|
||||
{
|
||||
body = body? body : [VectorL10n noticeFileAttachment];
|
||||
|
||||
NSDictionary *fileInfo = contentToUse[@"info"];
|
||||
NSDictionary *fileInfo;
|
||||
MXJSONModelSetDictionary(fileInfo, contentToUse[@"info"]);
|
||||
if (fileInfo)
|
||||
{
|
||||
NSNumber *fileSize = fileInfo[@"size"];
|
||||
NSNumber *fileSize;
|
||||
MXJSONModelSetNumber(fileSize, fileInfo[@"size"])
|
||||
if (fileSize)
|
||||
{
|
||||
body = [NSString stringWithFormat:@"%@ (%@)", body, [MXTools fileSizeToString: fileSize.longValue]];
|
||||
}
|
||||
else
|
||||
{
|
||||
MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported m.file format: %@", event.description);
|
||||
*error = MXKEventFormatterErrorUnsupported;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
// Attribute in an NSAttributeString that marks a blockquote block that was in the original HTML string.
|
||||
NSString *const kMXKToolsBlockquoteMarkAttribute = @"kMXKToolsBlockquoteMarkAttribute";
|
||||
|
||||
// Regex expression for permalink detection
|
||||
NSString *const kMXKToolsRegexStringForPermalink = @"(?:\\/.*)\\/#\\/(?:(?:room|user)\\/)?([^\\s]*)";
|
||||
|
||||
#pragma mark - MXKTools static private members
|
||||
// The regex used to find matrix ids.
|
||||
static NSRegularExpression *userIdRegex;
|
||||
@@ -47,6 +50,8 @@ static NSRegularExpression *httpLinksRegex;
|
||||
// A regex to find all HTML tags
|
||||
static NSRegularExpression *htmlTagsRegex;
|
||||
static NSDataDetector *linkDetector;
|
||||
// A regex to detect permalinks
|
||||
static NSRegularExpression* permalinkRegex;
|
||||
|
||||
@implementation MXKTools
|
||||
|
||||
@@ -63,6 +68,9 @@ static NSDataDetector *linkDetector;
|
||||
httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
|
||||
|
||||
NSString *permalinkPattern = [NSString stringWithFormat:@"%@%@", BWIBuildSettings.shared.clientPermalinkBaseUrl ?: kMXMatrixDotToUrl, kMXKToolsRegexStringForPermalink];
|
||||
permalinkRegex = [NSRegularExpression regularExpressionWithPattern:permalinkPattern options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1011,6 +1019,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
|
||||
+ (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString forEnabledMatrixIds:(NSInteger)enabledMatrixIdsBitMask
|
||||
{
|
||||
|
||||
if (!mutableAttributedString)
|
||||
{
|
||||
return;
|
||||
@@ -1039,10 +1048,30 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
{
|
||||
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex];
|
||||
}
|
||||
|
||||
// Permalinks
|
||||
NSArray* matches = [httpLinksRegex matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
|
||||
if (matches) {
|
||||
for (NSTextCheckingResult *match in matches)
|
||||
{
|
||||
NSRange matchRange = [match range];
|
||||
|
||||
NSString *link = [mutableAttributedString.string substringWithRange:matchRange];
|
||||
// Handle potential permalinks
|
||||
|
||||
if ([permalinkRegex numberOfMatchesInString:link options:0 range:NSMakeRange(0, link.length)]) {
|
||||
NSURLComponents *url = [[NSURLComponents new] initWithString:link];
|
||||
if (url.URL)
|
||||
{
|
||||
[mutableAttributedString addAttribute:NSLinkAttributeName value:url.URL range:matchRange];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This allows to check for normal url based links (like https://element.io)
|
||||
// And set back the default link color
|
||||
NSArray *matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
|
||||
matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
|
||||
if (matches)
|
||||
{
|
||||
for (NSTextCheckingResult *match in matches)
|
||||
|
||||
@@ -192,7 +192,13 @@ static NSAttributedString *verticalWhitespace = nil;
|
||||
NSString *claimedKey = _mxEvent.keysClaimed[@"ed25519"];
|
||||
NSString *algorithm = _mxEvent.wireContent[@"algorithm"];
|
||||
NSString *sessionId = _mxEvent.wireContent[@"session_id"];
|
||||
NSString *untrusted = _mxEvent.isUntrusted ? [VectorL10n roomEventEncryptionInfoKeyAuthenticityNotGuaranteed] : [VectorL10n userVerificationSessionsListSessionTrusted];
|
||||
NSString *safetyMessage = _mxEvent.decryptionDecoration.message;
|
||||
if (!safetyMessage)
|
||||
{
|
||||
// Use default copy if none is provided by the decryption decoration
|
||||
BOOL isUntrusted = _mxEvent.decryptionDecoration && _mxEvent.decryptionDecoration.color != MXEventDecryptionDecorationColorNone;
|
||||
safetyMessage = isUntrusted ? [VectorL10n roomEventEncryptionInfoKeyAuthenticityNotGuaranteed] : [VectorL10n userVerificationSessionsListSessionTrusted];
|
||||
}
|
||||
|
||||
NSString *decryptionError;
|
||||
if (_mxEvent.decryptionError)
|
||||
@@ -218,7 +224,8 @@ static NSAttributedString *verticalWhitespace = nil;
|
||||
}
|
||||
|
||||
[eventInformationString appendAttributedString:[[NSMutableAttributedString alloc]
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoEventUserId] attributes:@{NSForegroundColorAttributeName: _defaultTextColor,
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoEventUserId]
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor,
|
||||
NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]];
|
||||
[eventInformationString appendAttributedString:[[NSMutableAttributedString alloc]
|
||||
initWithString:senderId
|
||||
@@ -284,7 +291,7 @@ static NSAttributedString *verticalWhitespace = nil;
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor,
|
||||
NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]];
|
||||
[eventInformationString appendAttributedString:[[NSMutableAttributedString alloc]
|
||||
initWithString:untrusted
|
||||
initWithString:safetyMessage
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor,
|
||||
NSFontAttributeName: [UIFont systemFontOfSize:14]}]];
|
||||
[eventInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]];
|
||||
@@ -368,7 +375,8 @@ static NSAttributedString *verticalWhitespace = nil;
|
||||
[deviceInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]];
|
||||
|
||||
[deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc]
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoDeviceId] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]];
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoDeviceId]
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]];
|
||||
[deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc]
|
||||
initWithString:deviceId
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor,
|
||||
@@ -376,12 +384,14 @@ static NSAttributedString *verticalWhitespace = nil;
|
||||
[deviceInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]];
|
||||
|
||||
[deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc]
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoDeviceVerification] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]];
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoDeviceVerification]
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]];
|
||||
[deviceInformationString appendAttributedString:verification];
|
||||
[deviceInformationString appendAttributedString:[MXKEncryptionInfoView verticalWhitespace]];
|
||||
|
||||
[deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc]
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoDeviceFingerprint] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]];
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoDeviceFingerprint]
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont boldSystemFontOfSize:14]}]];
|
||||
[deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc]
|
||||
initWithString:fingerprint
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor,
|
||||
@@ -392,7 +402,8 @@ static NSAttributedString *verticalWhitespace = nil;
|
||||
{
|
||||
// Unknown device
|
||||
[deviceInformationString appendAttributedString:[[NSMutableAttributedString alloc]
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoDeviceUnknown] attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont italicSystemFontOfSize:14]}]];
|
||||
initWithString:[VectorL10n roomEventEncryptionInfoDeviceUnknown]
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor, NSFontAttributeName: [UIFont italicSystemFontOfSize:14]}]];
|
||||
}
|
||||
|
||||
[textViewAttributedString appendAttributedString:deviceInformationString];
|
||||
@@ -462,7 +473,8 @@ static NSAttributedString *verticalWhitespace = nil;
|
||||
{
|
||||
// Prompt user
|
||||
NSMutableAttributedString *textViewAttributedString = [[NSMutableAttributedString alloc]
|
||||
initWithString:[VectorL10n roomEventEncryptionVerifyTitle] attributes:@{NSForegroundColorAttributeName: _defaultTextColor,
|
||||
initWithString:[VectorL10n roomEventEncryptionVerifyTitle]
|
||||
attributes:@{NSForegroundColorAttributeName: _defaultTextColor,
|
||||
NSFontAttributeName: [UIFont boldSystemFontOfSize:17]}];
|
||||
|
||||
NSString *message = [VectorL10n roomEventEncryptionVerifyMessage:_mxDeviceInfo.displayName :_mxDeviceInfo.deviceId :_mxDeviceInfo.fingerprint];
|
||||
|
||||
@@ -45,8 +45,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||
private var navigationRouter: NavigationRouterType {
|
||||
parameters.router
|
||||
}
|
||||
// bwi: disable usage because of sideeffects when starting the app without serverurls (#4646)
|
||||
/// A strong ref to the legacy authVC as we need to init early to preload its view.
|
||||
private let legacyAuthenticationCoordinator: LegacyAuthenticationCoordinator
|
||||
private var legacyAuthenticationCoordinator: LegacyAuthenticationCoordinator?
|
||||
/// The currently active authentication coordinator, otherwise `nil`.
|
||||
private weak var authenticationCoordinator: AuthenticationCoordinatorProtocol?
|
||||
|
||||
@@ -84,11 +85,13 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||
init(parameters: OnboardingCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
// Preload the legacy authVC (it is *really* slow to load in realtime)
|
||||
let params = LegacyAuthenticationCoordinatorParameters(navigationRouter: parameters.router,
|
||||
canPresentAdditionalScreens: false)
|
||||
legacyAuthenticationCoordinator = LegacyAuthenticationCoordinator(parameters: params)
|
||||
|
||||
if !BWIBuildSettings.shared.disableLegacyAuthentication {
|
||||
// Preload the legacy authVC (it is *really* slow to load in realtime)
|
||||
let params = LegacyAuthenticationCoordinatorParameters(navigationRouter: parameters.router,
|
||||
canPresentAdditionalScreens: false)
|
||||
legacyAuthenticationCoordinator = LegacyAuthenticationCoordinator(parameters: params)
|
||||
}
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: parameters.router.toPresentable())
|
||||
|
||||
super.init()
|
||||
@@ -211,8 +214,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||
private func splashScreenCoordinator(_ coordinator: OnboardingSplashScreenCoordinator, didCompleteWith result: OnboardingSplashScreenViewModelResult) {
|
||||
splashScreenResult = result
|
||||
|
||||
// Set the auth type early on the legacy auth to allow network requests to finish during display of the use case screen.
|
||||
legacyAuthenticationCoordinator.update(authenticationFlow: result.flow)
|
||||
if !BWIBuildSettings.shared.disableLegacyAuthentication {
|
||||
// Set the auth type early on the legacy auth to allow network requests to finish during display of the use case screen.
|
||||
legacyAuthenticationCoordinator?.update(authenticationFlow: result.flow)
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .register:
|
||||
@@ -299,37 +304,42 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||
/// Show the legacy authentication screen. Any parameters that have been set in previous screens are be applied.
|
||||
/// - Parameter forceAsRootModule: Force setting the module as root instead of pushing
|
||||
private func showLegacyAuthenticationScreen(forceAsRootModule: Bool = false) {
|
||||
MXLog.debug("[OnboardingCoordinator] showLegacyAuthenticationScreen")
|
||||
|
||||
let coordinator = legacyAuthenticationCoordinator
|
||||
coordinator.callback = { [weak self, weak coordinator] result in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
if !BWIBuildSettings.shared.disableLegacyAuthentication {
|
||||
MXLog.debug("[OnboardingCoordinator] showLegacyAuthenticationScreen")
|
||||
|
||||
switch result {
|
||||
case .didLogin(let session, let authenticationFlow, let authenticationType):
|
||||
self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationFlow, using: authenticationType)
|
||||
case .didComplete:
|
||||
self.authenticationCoordinatorDidComplete(coordinator)
|
||||
case .clearAllData:
|
||||
self.showClearAllDataConfirmation()
|
||||
case .didStart, .cancel:
|
||||
// These results are only sent by the new flow.
|
||||
break
|
||||
guard let coordinator = legacyAuthenticationCoordinator else {
|
||||
return
|
||||
}
|
||||
}
|
||||
authenticationCoordinator = coordinator
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.callback = { [weak self, weak coordinator] result in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
|
||||
switch result {
|
||||
case .didLogin(let session, let authenticationFlow, let authenticationType):
|
||||
self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationFlow, using: authenticationType)
|
||||
case .didComplete:
|
||||
self.authenticationCoordinatorDidComplete(coordinator)
|
||||
case .clearAllData:
|
||||
self.showClearAllDataConfirmation()
|
||||
case .didStart, .cancel:
|
||||
// These results are only sent by the new flow.
|
||||
break
|
||||
}
|
||||
}
|
||||
authenticationCoordinator = coordinator
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
|
||||
if navigationRouter.modules.isEmpty || forceAsRootModule || BWIBuildSettings.shared.showBUMLottieAnimation {
|
||||
navigationRouter.setRootModule(coordinator, popCompletion: nil)
|
||||
} else {
|
||||
navigationRouter.push(coordinator, animated: true) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
if navigationRouter.modules.isEmpty || forceAsRootModule || BWIBuildSettings.shared.showBUMLottieAnimation {
|
||||
navigationRouter.setRootModule(coordinator, popCompletion: nil)
|
||||
} else {
|
||||
navigationRouter.push(coordinator, animated: true) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
stopLoading()
|
||||
|
||||
}
|
||||
|
||||
/// Cancels the registration flow, returning to the Use Case screen.
|
||||
|
||||
@@ -25,7 +25,9 @@ class PillAttachmentView: UIView {
|
||||
struct Sizes {
|
||||
var verticalMargin: CGFloat
|
||||
var horizontalMargin: CGFloat
|
||||
var avatarLeading: CGFloat
|
||||
var avatarSideLength: CGFloat
|
||||
var itemSpacing: CGFloat
|
||||
|
||||
var pillBackgroundHeight: CGFloat {
|
||||
return avatarSideLength + 2 * verticalMargin
|
||||
@@ -33,11 +35,8 @@ class PillAttachmentView: UIView {
|
||||
var pillHeight: CGFloat {
|
||||
return pillBackgroundHeight + 2 * verticalMargin
|
||||
}
|
||||
var displaynameLabelLeading: CGFloat {
|
||||
return avatarSideLength + 2 * horizontalMargin
|
||||
}
|
||||
var totalWidthWithoutLabel: CGFloat {
|
||||
return displaynameLabelLeading + 2 * horizontalMargin
|
||||
return avatarSideLength + 2 * horizontalMargin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,44 +55,112 @@ class PillAttachmentView: UIView {
|
||||
mediaManager: MXMediaManager?,
|
||||
andPillData pillData: PillTextAttachmentData) {
|
||||
self.init(frame: frame)
|
||||
let label = UILabel(frame: .zero)
|
||||
label.text = pillData.displayText
|
||||
label.font = pillData.font
|
||||
label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor
|
||||
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
||||
height: sizes.pillBackgroundHeight))
|
||||
label.frame = CGRect(x: sizes.displaynameLabelLeading,
|
||||
y: 0,
|
||||
width: labelSize.width,
|
||||
height: sizes.pillBackgroundHeight)
|
||||
|
||||
let stack = UIStackView(frame: frame)
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.spacing = sizes.itemSpacing
|
||||
|
||||
var computedWidth: CGFloat = 0
|
||||
for item in pillData.items {
|
||||
switch item {
|
||||
case .text(let string):
|
||||
let label = UILabel(frame: .zero)
|
||||
label.text = string
|
||||
label.font = pillData.font
|
||||
label.textColor = pillData.isHighlighted ? theme.baseTextPrimaryColor : theme.textPrimaryColor
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.addArrangedSubview(label)
|
||||
|
||||
computedWidth += label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: sizes.pillBackgroundHeight)).width
|
||||
|
||||
case .avatar(let url, let alt, let matrixId):
|
||||
let avatarView = UserAvatarView(frame: CGRect(origin: .zero, size: CGSize(width: sizes.avatarSideLength, height: sizes.avatarSideLength)))
|
||||
|
||||
avatarView.fill(with: AvatarViewData(matrixItemId: matrixId,
|
||||
displayName: alt,
|
||||
avatarUrl: url,
|
||||
mediaManager: mediaManager,
|
||||
fallbackImage: .matrixItem(matrixId, alt)))
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.addArrangedSubview(avatarView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
|
||||
avatarView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
|
||||
])
|
||||
|
||||
computedWidth += sizes.avatarSideLength
|
||||
|
||||
case .spaceAvatar(let url, let alt, let matrixId):
|
||||
let avatarView = SpaceAvatarView(frame: CGRect(origin: .zero, size: CGSize(width: sizes.avatarSideLength, height: sizes.avatarSideLength)))
|
||||
|
||||
avatarView.fill(with: AvatarViewData(matrixItemId: matrixId,
|
||||
displayName: alt,
|
||||
avatarUrl: url,
|
||||
mediaManager: mediaManager,
|
||||
fallbackImage: .matrixItem(matrixId, alt)))
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.addArrangedSubview(avatarView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
|
||||
avatarView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
|
||||
])
|
||||
|
||||
computedWidth += sizes.avatarSideLength
|
||||
|
||||
case .asset(let name, let parameters):
|
||||
let assetView = UIView(frame: CGRect(x: 0, y: 0, width: sizes.avatarSideLength, height: sizes.avatarSideLength))
|
||||
assetView.backgroundColor = parameters.backgroundColor?.uiColor
|
||||
assetView.layer.cornerRadius = sizes.avatarSideLength / 2
|
||||
assetView.isUserInteractionEnabled = false
|
||||
assetView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let imageView = UIImageView(frame: .zero)
|
||||
imageView.image = ImageAsset(name: name).image.withRenderingMode(UIImage.RenderingMode(rawValue: parameters.rawRenderingMode) ?? .automatic)
|
||||
imageView.tintColor = parameters.tintColor?.uiColor ?? theme.baseIconPrimaryColor
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
|
||||
assetView.vc_addSubViewMatchingParent(imageView, withInsets: UIEdgeInsets(top: parameters.padding, left: parameters.padding, bottom: -parameters.padding, right: -parameters.padding))
|
||||
|
||||
stack.addArrangedSubview(assetView)
|
||||
NSLayoutConstraint.activate([
|
||||
assetView.widthAnchor.constraint(equalToConstant: sizes.avatarSideLength),
|
||||
assetView.heightAnchor.constraint(equalToConstant: sizes.avatarSideLength)
|
||||
])
|
||||
|
||||
computedWidth += sizes.avatarSideLength
|
||||
}
|
||||
}
|
||||
computedWidth += max(0, CGFloat(stack.arrangedSubviews.count - 1) * stack.spacing)
|
||||
|
||||
let leadingStackMargin: CGFloat
|
||||
switch pillData.items.first {
|
||||
case .asset, .avatar:
|
||||
leadingStackMargin = sizes.avatarLeading
|
||||
computedWidth += sizes.avatarLeading + sizes.horizontalMargin
|
||||
default:
|
||||
leadingStackMargin = sizes.horizontalMargin
|
||||
computedWidth += 2 * sizes.horizontalMargin
|
||||
}
|
||||
|
||||
computedWidth = min(pillData.maxWidth, computedWidth)
|
||||
|
||||
let pillBackgroundView = UIView(frame: CGRect(x: 0,
|
||||
y: sizes.verticalMargin,
|
||||
width: labelSize.width + sizes.totalWidthWithoutLabel,
|
||||
height: sizes.pillBackgroundHeight))
|
||||
|
||||
let avatarView = UserAvatarView(frame: CGRect(x: sizes.horizontalMargin,
|
||||
y: sizes.verticalMargin,
|
||||
width: sizes.avatarSideLength,
|
||||
height: sizes.avatarSideLength))
|
||||
|
||||
avatarView.fill(with: AvatarViewData(matrixItemId: pillData.matrixItemId,
|
||||
displayName: pillData.displayName,
|
||||
avatarUrl: pillData.avatarUrl,
|
||||
mediaManager: mediaManager,
|
||||
fallbackImage: .matrixItem(pillData.matrixItemId, pillData.displayName)))
|
||||
avatarView.isUserInteractionEnabled = false
|
||||
|
||||
pillBackgroundView.addSubview(avatarView)
|
||||
pillBackgroundView.addSubview(label)
|
||||
width: computedWidth,
|
||||
height: sizes.pillBackgroundHeight))
|
||||
|
||||
pillBackgroundView.vc_addSubViewMatchingParent(stack, withInsets: UIEdgeInsets(top: sizes.verticalMargin, left: leadingStackMargin, bottom: -sizes.verticalMargin, right: -sizes.horizontalMargin))
|
||||
|
||||
pillBackgroundView.backgroundColor = pillData.isHighlighted ? theme.colors.alert : theme.colors.quinaryContent
|
||||
pillBackgroundView.layer.cornerRadius = sizes.pillBackgroundHeight / 2.0
|
||||
|
||||
self.addSubview(pillBackgroundView)
|
||||
self.alpha = pillData.alpha
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Override
|
||||
override var isHidden: Bool {
|
||||
get {
|
||||
|
||||
@@ -20,9 +20,11 @@ import UIKit
|
||||
@available(iOS 15.0, *)
|
||||
@objc class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
||||
// MARK: - Properties
|
||||
private static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
|
||||
horizontalMargin: 4.0,
|
||||
avatarSideLength: 16.0)
|
||||
static let pillAttachmentViewSizes = PillAttachmentView.Sizes(verticalMargin: 2.0,
|
||||
horizontalMargin: 6.0,
|
||||
avatarLeading: 2.0,
|
||||
avatarSideLength: 16.0,
|
||||
itemSpacing: 4)
|
||||
private weak var messageTextView: MXKMessageTextView?
|
||||
|
||||
// MARK: - Override
|
||||
@@ -40,15 +42,18 @@ import UIKit
|
||||
return
|
||||
}
|
||||
|
||||
guard let pillData = textAttachment.data else {
|
||||
guard var pillData = textAttachment.data else {
|
||||
MXLog.debug("[PillAttachmentViewProvider]: attachment misses pill data")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let messageTextView {
|
||||
pillData.maxWidth = messageTextView.bounds.width - 8
|
||||
}
|
||||
|
||||
let mainSession = AppDelegate.theDelegate().mxSessions.first as? MXSession
|
||||
|
||||
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: Self.size(forDisplayText: pillData.displayText,
|
||||
andFont: pillData.font)),
|
||||
let pillView = PillAttachmentView(frame: CGRect(origin: .zero, size: textAttachment.size(forFont: pillData.font)),
|
||||
sizes: Self.pillAttachmentViewSizes,
|
||||
theme: ThemeService.shared().theme,
|
||||
mediaManager: mainSession?.mediaManager,
|
||||
@@ -57,23 +62,3 @@ import UIKit
|
||||
messageTextView?.registerPillView(pillView)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
extension PillAttachmentViewProvider {
|
||||
/// Computes size required to display a pill for given display text.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - displayText: display text for the pill
|
||||
/// - font: the text font
|
||||
/// - Returns: required size for pill
|
||||
static func size(forDisplayText displayText: String, andFont font: UIFont) -> CGSize {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.text = displayText
|
||||
label.font = font
|
||||
let labelSize = label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
||||
height: pillAttachmentViewSizes.pillBackgroundHeight))
|
||||
|
||||
return CGSize(width: labelSize.width + pillAttachmentViewSizes.totalWidthWithoutLabel,
|
||||
height: pillAttachmentViewSizes.pillHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
private enum PillAttachmentKind {
|
||||
case attachment(PillTextAttachment)
|
||||
case string(NSAttributedString)
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
struct PillProvider {
|
||||
private let session: MXSession
|
||||
private let eventFormatter: MXKEventFormatter
|
||||
private let event: MXEvent
|
||||
private let roomState: MXRoomState
|
||||
private let latestRoomState: MXRoomState?
|
||||
private let isEditMode: Bool
|
||||
|
||||
init(withSession session: MXSession,
|
||||
eventFormatter: MXKEventFormatter,
|
||||
event: MXEvent,
|
||||
roomState: MXRoomState,
|
||||
andLatestRoomState latestRoomState: MXRoomState?,
|
||||
isEditMode: Bool) {
|
||||
|
||||
self.session = session
|
||||
self.eventFormatter = eventFormatter
|
||||
self.event = event
|
||||
self.roomState = roomState
|
||||
self.latestRoomState = latestRoomState
|
||||
self.isEditMode = isEditMode
|
||||
}
|
||||
|
||||
func pillTextAttachmentString(forUrl url: URL, withLabel label: String, event: MXEvent) -> NSAttributedString? {
|
||||
|
||||
// Try to get a pill from this url
|
||||
guard let pillType = PillType.from(url: url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not pillify an url if it is a markdown or an http link (except for user and room) with a custom text
|
||||
|
||||
// First, we need to handle the case where the label can contains more than one # (room alias)
|
||||
var urlFromLabel = URL(string: label)?.absoluteURL
|
||||
if urlFromLabel == nil, label.filter({ $0 == "#" }).count > 1 {
|
||||
if let escapedLabel = label.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedLabel) {
|
||||
urlFromLabel = Tools.fixURL(withSeveralHashKeys: url)
|
||||
}
|
||||
}
|
||||
|
||||
let fixedUrl = Tools.fixURL(withSeveralHashKeys: url)
|
||||
let isUrlMarkDownLink = urlFromLabel != fixedUrl
|
||||
|
||||
let result: PillAttachmentKind
|
||||
switch pillType {
|
||||
case .user(let userId):
|
||||
var userFound = false
|
||||
result = pillTextAttachment(forUserId: userId, userFound: &userFound)
|
||||
// if it is a markdown link and we didn't found the user, don't pillify it
|
||||
if isUrlMarkDownLink && !userFound {
|
||||
return nil
|
||||
}
|
||||
case .room(let roomId):
|
||||
var roomFound = false
|
||||
result = pillTextAttachment(forRoomId: roomId, roomFound: &roomFound)
|
||||
// if it is a markdown link and we didn't found the room, don't pillify it
|
||||
if isUrlMarkDownLink && !roomFound {
|
||||
return nil
|
||||
}
|
||||
case .message(let roomId, let messageId):
|
||||
// if it is a markdown link, don't pillify it
|
||||
if isUrlMarkDownLink {
|
||||
return nil
|
||||
}
|
||||
result = pillTextAttachment(forMessageId: messageId, inRoomId: roomId)
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .attachment(let pillTextAttachment):
|
||||
return PillsFormatter.attributedStringWithAttachment(pillTextAttachment, link: isEditMode ? nil : url, font: eventFormatter.defaultTextFont)
|
||||
case .string(let attributedString):
|
||||
// if we don't have an attachment, use the fallback attributed string
|
||||
let newAttrString = NSMutableAttributedString(attributedString: attributedString)
|
||||
if let font = eventFormatter.defaultTextFont {
|
||||
newAttrString.addAttribute(.font, value: font, range: .init(location: 0, length: newAttrString.length))
|
||||
}
|
||||
newAttrString.addAttribute(.foregroundColor, value: ThemeService.shared().theme.colors.links, range: .init(location: 0, length: newAttrString.length))
|
||||
newAttrString.addAttribute(.link, value: url, range: .init(location: 0, length: newAttrString.length))
|
||||
return newAttrString
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the latest available `MXRoomMember` from given data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userId: the id of the user
|
||||
/// - Returns: the room member, if available
|
||||
private func roomMember(withUserId userId: String) -> MXRoomMember? {
|
||||
return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId)
|
||||
}
|
||||
|
||||
/// Create a pill representation for a given user
|
||||
/// - Parameters:
|
||||
/// - userId: the user MatrixID
|
||||
/// - userFound: this flag will be set to true if a user is found locally with this userId
|
||||
/// - Returns: a pill attachment
|
||||
private func pillTextAttachment(forUserId userId: String, userFound: inout Bool) -> PillAttachmentKind {
|
||||
// Search for a room member matching this user id
|
||||
let roomMember = self.roomMember(withUserId: userId)
|
||||
var user: MXUser?
|
||||
|
||||
if roomMember == nil {
|
||||
// fallback on getting the user from the session's store
|
||||
user = session.user(withUserId: userId)
|
||||
}
|
||||
|
||||
|
||||
let avatarUrl = roomMember?.avatarUrl ?? user?.avatarUrl
|
||||
let displayName = roomMember?.displayname ?? user?.displayName ?? userId
|
||||
let isHighlighted = userId == session.myUserId
|
||||
|
||||
let avatar: PillTextAttachmentItem
|
||||
if roomMember == nil && user == nil {
|
||||
avatar = .asset(named: "pill_user",
|
||||
parameters: .init(tintColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.secondaryContent),
|
||||
rawRenderingMode: UIImage.RenderingMode.alwaysOriginal.rawValue,
|
||||
padding: 0.0))
|
||||
} else {
|
||||
avatar = .avatar(url: avatarUrl,
|
||||
string: displayName,
|
||||
matrixId: userId)
|
||||
}
|
||||
|
||||
let data = PillTextAttachmentData(pillType: .user(userId: userId),
|
||||
items: [
|
||||
avatar,
|
||||
.text(displayName)
|
||||
],
|
||||
isHighlighted: isHighlighted,
|
||||
alpha: 1.0,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
userFound = roomMember != nil || user != nil
|
||||
|
||||
if let attachment = PillTextAttachment(attachmentData: data) {
|
||||
return .attachment(attachment)
|
||||
}
|
||||
|
||||
return .string(NSMutableAttributedString(string: displayName))
|
||||
}
|
||||
|
||||
/// Create a pill representation for a given room
|
||||
/// - Parameters:
|
||||
/// - roomId: the room MXID or alias
|
||||
/// - roomFound: this flag will be set to true if a room is found locally with this roomId
|
||||
/// - Returns: a pill attachment
|
||||
private func pillTextAttachment(forRoomId roomId: String, roomFound: inout Bool) -> PillAttachmentKind {
|
||||
// Get the room matching this roomId
|
||||
let room = roomId.starts(with: "#") ? session.room(withAlias: roomId) : session.room(withRoomId: roomId)
|
||||
let displayName = room?.displayName ?? VectorL10n.pillRoomFallbackDisplayName
|
||||
|
||||
let avatar: PillTextAttachmentItem
|
||||
if let room {
|
||||
if session.spaceService.getSpace(withId: roomId) != nil {
|
||||
avatar = .spaceAvatar(url: room.avatarData.mxContentUri,
|
||||
string: displayName,
|
||||
matrixId: roomId)
|
||||
} else {
|
||||
avatar = .avatar(url: room.avatarData.mxContentUri,
|
||||
string: displayName,
|
||||
matrixId: roomId)
|
||||
}
|
||||
} else {
|
||||
avatar = .asset(named: "link_icon",
|
||||
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
|
||||
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
|
||||
}
|
||||
|
||||
let data = PillTextAttachmentData(pillType: .room(roomId: roomId),
|
||||
items: [
|
||||
avatar,
|
||||
.text(displayName)
|
||||
],
|
||||
isHighlighted: false,
|
||||
alpha: 1.0,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
roomFound = room != nil
|
||||
|
||||
if let attachment = PillTextAttachment(attachmentData: data) {
|
||||
return .attachment(attachment)
|
||||
}
|
||||
|
||||
return .string(NSMutableAttributedString(string: displayName))
|
||||
}
|
||||
|
||||
/// Create a pill representation for a message in a room
|
||||
/// - Parameters:
|
||||
/// - messageId: message eventId
|
||||
/// - roomId: roomId of the message
|
||||
/// - Returns: a pill attachment
|
||||
private func pillTextAttachment(forMessageId messageId: String, inRoomId roomId: String) -> PillAttachmentKind {
|
||||
|
||||
// Check if this is the current room
|
||||
if roomId == roomState.roomId {
|
||||
return pillTextAttachment(inCurrentRoomForMessageId: messageId)
|
||||
}
|
||||
|
||||
let room = session.room(withRoomId: roomId)
|
||||
|
||||
let avatar: PillTextAttachmentItem
|
||||
if let room {
|
||||
avatar = .avatar(url: room.avatarData.mxContentUri,
|
||||
string: room.displayName,
|
||||
matrixId: roomId)
|
||||
} else {
|
||||
avatar = .asset(named: "link_icon",
|
||||
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
|
||||
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
|
||||
|
||||
}
|
||||
|
||||
let displayText = room?.displayName.flatMap { VectorL10n.pillMessageIn($0) } ?? VectorL10n.pillMessage
|
||||
|
||||
let data = PillTextAttachmentData(pillType: .message(roomId: roomId, eventId: messageId),
|
||||
items: [
|
||||
avatar,
|
||||
.text(displayText)
|
||||
],
|
||||
isHighlighted: false,
|
||||
alpha: 1.0,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
if let attachment = PillTextAttachment(attachmentData: data) {
|
||||
return .attachment(attachment)
|
||||
}
|
||||
|
||||
return .string(NSMutableAttributedString(string: displayText))
|
||||
}
|
||||
|
||||
/// Create a pill representation for a message in the current room
|
||||
/// - Parameters:
|
||||
/// - messageId: message eventId
|
||||
/// - Returns: a pill attachment
|
||||
private func pillTextAttachment(inCurrentRoomForMessageId messageId: String) -> PillAttachmentKind {
|
||||
var roomMember: MXRoomMember?
|
||||
// If we have the event locally, try to get the room member
|
||||
if let event = session.store.event(withEventId: messageId, inRoom: roomState.roomId) {
|
||||
roomMember = self.roomMember(withUserId: event.sender)
|
||||
}
|
||||
|
||||
let displayText: String
|
||||
let avatar: PillTextAttachmentItem
|
||||
if let roomMember {
|
||||
displayText = VectorL10n.pillMessageFrom(roomMember.displayname ?? roomMember.userId)
|
||||
avatar = .avatar(url: roomMember.avatarUrl,
|
||||
string: roomMember.displayname,
|
||||
matrixId: roomMember.userId)
|
||||
} else {
|
||||
displayText = VectorL10n.pillMessage
|
||||
avatar = .asset(named: "link_icon",
|
||||
parameters: .init(backgroundColor: PillAssetColor(uiColor: ThemeService.shared().theme.colors.links),
|
||||
rawRenderingMode: UIImage.RenderingMode.alwaysTemplate.rawValue))
|
||||
}
|
||||
|
||||
let data = PillTextAttachmentData(pillType: .message(roomId: roomState.roomId, eventId: messageId),
|
||||
items: [
|
||||
avatar,
|
||||
.text(displayText)
|
||||
].compactMap { $0 },
|
||||
isHighlighted: false,
|
||||
alpha: 1.0,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
if let attachment = PillTextAttachment(attachmentData: data) {
|
||||
return .attachment(attachment)
|
||||
}
|
||||
|
||||
return .string(NSMutableAttributedString(string: displayText))
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ class PillTextAttachment: NSTextAttachment {
|
||||
|
||||
updateBounds()
|
||||
}
|
||||
|
||||
convenience init?(attachmentData: PillTextAttachmentData) {
|
||||
guard let encodedData = try? Self.serializationService.serialize(attachmentData) else { return nil }
|
||||
self.init(data: encodedData, ofType: PillsFormatter.pillUTType)
|
||||
}
|
||||
|
||||
/// Create a Mention Pill text attachment for given room member.
|
||||
///
|
||||
@@ -55,9 +60,13 @@ class PillTextAttachment: NSTextAttachment {
|
||||
convenience init?(withRoomMember roomMember: MXRoomMember,
|
||||
isHighlighted: Bool,
|
||||
font: UIFont) {
|
||||
let data = PillTextAttachmentData(matrixItemId: roomMember.userId,
|
||||
displayName: roomMember.displayname,
|
||||
avatarUrl: roomMember.avatarUrl,
|
||||
let data = PillTextAttachmentData(pillType: .user(userId: roomMember.userId),
|
||||
items: [
|
||||
.avatar(url: roomMember.avatarUrl,
|
||||
string: roomMember.displayname,
|
||||
matrixId: roomMember.userId),
|
||||
.text(roomMember.displayname)
|
||||
],
|
||||
isHighlighted: isHighlighted,
|
||||
alpha: 1.0,
|
||||
font: font)
|
||||
@@ -71,14 +80,65 @@ class PillTextAttachment: NSTextAttachment {
|
||||
|
||||
updateBounds()
|
||||
}
|
||||
|
||||
/// Computes size required to display a pill for given display text.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - font: the text font
|
||||
/// - Returns: required size for pill
|
||||
func size(forFont font: UIFont) -> CGSize {
|
||||
guard let data else {
|
||||
MXLog.debug("[PillTextAttachment]: data are missing")
|
||||
return .zero
|
||||
}
|
||||
|
||||
let sizes = PillAttachmentViewProvider.pillAttachmentViewSizes
|
||||
|
||||
var width: CGFloat = 0
|
||||
|
||||
var textContent = ""
|
||||
for item in data.items {
|
||||
switch item {
|
||||
case .text(let text):
|
||||
textContent += text
|
||||
case .avatar, .asset, .spaceAvatar:
|
||||
width += sizes.avatarSideLength
|
||||
}
|
||||
}
|
||||
|
||||
// add texts
|
||||
if !textContent.isEmpty {
|
||||
let label = UILabel(frame: .zero)
|
||||
label.font = font
|
||||
label.text = textContent
|
||||
width += label.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude,
|
||||
height: sizes.pillBackgroundHeight)).width
|
||||
}
|
||||
|
||||
// add spacing
|
||||
width += CGFloat(max(0, data.items.count - 1)) * sizes.itemSpacing
|
||||
// add margins
|
||||
switch data.items.first {
|
||||
case .asset, .avatar:
|
||||
width += sizes.avatarLeading + sizes.horizontalMargin
|
||||
default:
|
||||
width += 2 * sizes.horizontalMargin
|
||||
}
|
||||
|
||||
width = min(width, data.maxWidth)
|
||||
|
||||
return CGSize(width: width,
|
||||
height: sizes.pillHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@available (iOS 15.0, *)
|
||||
private extension PillTextAttachment {
|
||||
|
||||
func updateBounds() {
|
||||
guard let data = data else { return }
|
||||
let pillSize = PillAttachmentViewProvider.size(forDisplayText: data.displayText, andFont: data.font)
|
||||
let pillSize = size(forFont: data.font)
|
||||
// Offset to align pill centerY with text centerY.
|
||||
let offset = data.font.descender + (data.font.lineHeight - pillSize.height) / 2.0
|
||||
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: pillSize)
|
||||
|
||||
@@ -17,62 +17,98 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
struct PillAssetColor: Codable {
|
||||
var red: CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0
|
||||
|
||||
var uiColor: UIColor {
|
||||
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
|
||||
init(uiColor: UIColor) {
|
||||
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||
}
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
struct PillAssetParameter: Codable {
|
||||
var tintColor: PillAssetColor?
|
||||
var backgroundColor: PillAssetColor?
|
||||
var rawRenderingMode: Int = UIImage.RenderingMode.automatic.rawValue
|
||||
var padding: CGFloat = 2.0
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
enum PillTextAttachmentItem: Codable {
|
||||
case text(String)
|
||||
case avatar(url: String?, string: String?, matrixId: String)
|
||||
case spaceAvatar(url: String?, string: String?, matrixId: String)
|
||||
case asset(named: String, parameters: PillAssetParameter)
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
extension PillTextAttachmentItem {
|
||||
var string: String? {
|
||||
switch self {
|
||||
case .text(let text):
|
||||
return text
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data associated with a Pill text attachment.
|
||||
@available (iOS 15.0, *)
|
||||
struct PillTextAttachmentData: Codable {
|
||||
// MARK: - Properties
|
||||
/// Matrix item identifier (user id or room id)
|
||||
var matrixItemId: String
|
||||
/// Matrix item display name (user or room display name)
|
||||
var displayName: String?
|
||||
/// Matrix item avatar URL (user or room avatar url)
|
||||
var avatarUrl: String?
|
||||
/// Pill type
|
||||
var pillType: PillType
|
||||
/// Items to render
|
||||
var items: [PillTextAttachmentItem]
|
||||
/// Wether the pill should be highlighted
|
||||
var isHighlighted: Bool
|
||||
/// Alpha for pill display
|
||||
var alpha: CGFloat
|
||||
/// Font for the display name
|
||||
var font: UIFont
|
||||
/// Max width
|
||||
var maxWidth: CGFloat
|
||||
|
||||
/// Helper for preferred text to display.
|
||||
var displayText: String {
|
||||
guard let displayName = displayName,
|
||||
displayName.count > 0 else {
|
||||
return matrixItemId
|
||||
}
|
||||
|
||||
return displayName
|
||||
return items.map { $0.string }
|
||||
.compactMap { $0 }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
/// Init.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - matrixItemId: Matrix item identifier (user id or room id)
|
||||
/// - displayName: Matrix item display name (user or room display name)
|
||||
/// - avatarUrl: Matrix item avatar URL (user or room avatar url)
|
||||
/// - pillType: Type for the pill
|
||||
/// - items: Items to display
|
||||
/// - isHighlighted: Wether the pill should be highlighted
|
||||
/// - alpha: Alpha for pill display
|
||||
/// - font: Font for the display name
|
||||
init(matrixItemId: String,
|
||||
displayName: String?,
|
||||
avatarUrl: String?,
|
||||
init(pillType: PillType,
|
||||
items: [PillTextAttachmentItem],
|
||||
isHighlighted: Bool,
|
||||
alpha: CGFloat,
|
||||
font: UIFont) {
|
||||
self.matrixItemId = matrixItemId
|
||||
self.displayName = displayName
|
||||
self.avatarUrl = avatarUrl
|
||||
font: UIFont,
|
||||
maxWidth: CGFloat = .greatestFiniteMagnitude) {
|
||||
self.pillType = pillType
|
||||
self.items = items
|
||||
self.isHighlighted = isHighlighted
|
||||
self.alpha = alpha
|
||||
self.font = font
|
||||
self.maxWidth = maxWidth
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Codable
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case matrixItemId
|
||||
case displayName
|
||||
case avatarUrl
|
||||
case pillType
|
||||
case items
|
||||
case isHighlighted
|
||||
case alpha
|
||||
case font
|
||||
@@ -84,9 +120,8 @@ struct PillTextAttachmentData: Codable {
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
matrixItemId = try container.decode(String.self, forKey: .matrixItemId)
|
||||
displayName = try? container.decode(String.self, forKey: .displayName)
|
||||
avatarUrl = try? container.decode(String.self, forKey: .avatarUrl)
|
||||
pillType = try container.decode(PillType.self, forKey: .pillType)
|
||||
items = try container.decode([PillTextAttachmentItem].self, forKey: .items)
|
||||
isHighlighted = try container.decode(Bool.self, forKey: .isHighlighted)
|
||||
alpha = try container.decode(CGFloat.self, forKey: .alpha)
|
||||
let fontData = try container.decode(Data.self, forKey: .font)
|
||||
@@ -95,16 +130,41 @@ struct PillTextAttachmentData: Codable {
|
||||
} else {
|
||||
throw PillTextAttachmentDataError.noFontData
|
||||
}
|
||||
maxWidth = .greatestFiniteMagnitude
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(matrixItemId, forKey: .matrixItemId)
|
||||
try? container.encode(displayName, forKey: .displayName)
|
||||
try? container.encode(avatarUrl, forKey: .avatarUrl)
|
||||
try container.encode(pillType, forKey: .pillType)
|
||||
try container.encode(items, forKey: .items)
|
||||
try container.encode(isHighlighted, forKey: .isHighlighted)
|
||||
try container.encode(alpha, forKey: .alpha)
|
||||
let fontData = try NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false)
|
||||
try container.encode(fontData, forKey: .font)
|
||||
}
|
||||
|
||||
// MARK: - Pill representations
|
||||
var pillIdentifier: String {
|
||||
switch pillType {
|
||||
case .user(let userId):
|
||||
return userId
|
||||
case .room(let roomId):
|
||||
return roomId
|
||||
case .message(let roomId, let messageId):
|
||||
return "\(roomId)/\(messageId)"
|
||||
}
|
||||
}
|
||||
|
||||
var markdown: String {
|
||||
var permalink: String
|
||||
switch pillType {
|
||||
case .user(let userId):
|
||||
permalink = MXTools.permalinkToUser(withUserId: userId)
|
||||
case .room(let roomId):
|
||||
permalink = MXTools.permalink(toRoom: roomId)
|
||||
case .message(let roomId, let messageId):
|
||||
permalink = MXTools.permalink(toEvent: messageId, inRoom: roomId)
|
||||
}
|
||||
return "[\(displayText)](\(permalink))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
enum PillType: Codable {
|
||||
case user(userId: String) /// userId
|
||||
case room(roomId: String) /// roomId
|
||||
case message(roomId: String, eventId: String) // roomId, eventId
|
||||
}
|
||||
|
||||
@available (iOS 15.0, *)
|
||||
extension PillType {
|
||||
private static var regexPermalinkTarget: NSRegularExpression? = {
|
||||
let clientBaseUrl = BWIBuildSettings.shared.clientPermalinkBaseUrl
|
||||
let pattern = #"\#(clientBaseUrl)(?:/.*)/#/(?:(?:room|user)/)?((?:@|!|#)[^@!#/?\s]*)/?((?:\$)[^\$/?\s]*)?"#
|
||||
return try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
|
||||
}()
|
||||
|
||||
static func from(url: URL) -> PillType? {
|
||||
guard let regex = regexPermalinkTarget else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var link = url.absoluteString
|
||||
// we need to remove percent encoding (it's possible that it has been encoded multiple times)
|
||||
while let cleaned = link.removingPercentEncoding, cleaned != link {
|
||||
link = cleaned
|
||||
}
|
||||
|
||||
let pills = regex.matches(in: link, options: [], range: NSRange(link.startIndex..., in: link))
|
||||
.map { result -> [String]? in
|
||||
guard result.numberOfRanges > 1 else { return nil }
|
||||
return (1..<result.numberOfRanges)
|
||||
.map { Range(result.range(at: $0), in: link) }
|
||||
.compactMap { $0 }
|
||||
.map { String(link[$0]).removingPercentEncoding }
|
||||
.compactMap { $0 }
|
||||
|
||||
}
|
||||
.compactMap { matrixIds -> PillType? in
|
||||
guard let matrixIds, !matrixIds.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
switch matrixIds[0].first {
|
||||
case "@":
|
||||
return .user(userId: matrixIds[0])
|
||||
case "!", "#":
|
||||
if matrixIds.count > 1 {
|
||||
if matrixIds[1].starts(with: "$") {
|
||||
return .message(roomId: matrixIds[0], eventId: matrixIds[1])
|
||||
}
|
||||
}
|
||||
return .room(roomId: matrixIds[0])
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return pills.first
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class PillsFormatter: NSObject {
|
||||
case identifier
|
||||
case markdown
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Internal Methods
|
||||
/// Insert text attachments for pills inside given message attributed string.
|
||||
///
|
||||
@@ -52,17 +52,21 @@ class PillsFormatter: NSObject {
|
||||
roomState: MXRoomState,
|
||||
andLatestRoomState latestRoomState: MXRoomState?,
|
||||
isEditMode: Bool = false) -> NSAttributedString {
|
||||
|
||||
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
||||
newAttr.vc_enumerateAttribute(.link) { (url: URL, range: NSRange, _) in
|
||||
if let userId = userIdFromPermalink(url.absoluteString),
|
||||
let roomMember = roomMember(withUserId: userId,
|
||||
roomState: roomState,
|
||||
andLatestRoomState: latestRoomState) {
|
||||
let isHighlighted = roomMember.userId == session.myUserId && event.sender != session.myUserId
|
||||
let attachmentString = mentionPill(withRoomMember: roomMember,
|
||||
andUrl: isEditMode ? nil : url,
|
||||
isHighlighted: isHighlighted,
|
||||
font: eventFormatter.defaultTextFont)
|
||||
|
||||
let provider = PillProvider(withSession: session,
|
||||
eventFormatter: eventFormatter,
|
||||
event: event,
|
||||
roomState: roomState,
|
||||
andLatestRoomState: latestRoomState,
|
||||
isEditMode: isEditMode)
|
||||
|
||||
// try to get a mention pill from the url
|
||||
let label = Range(range, in: newAttr.string).flatMap { String(newAttr.string[$0]) }
|
||||
if let attachmentString: NSAttributedString = provider.pillTextAttachmentString(forUrl: url, withLabel: label ?? "", event: event) {
|
||||
// replace the url with the pill
|
||||
newAttr.replaceCharacters(in: range, with: attachmentString)
|
||||
}
|
||||
}
|
||||
@@ -80,25 +84,27 @@ class PillsFormatter: NSObject {
|
||||
mode: PillsReplacementTextMode = .displayname) -> String {
|
||||
let newAttr = NSMutableAttributedString(attributedString: attributedString)
|
||||
newAttr.vc_enumerateAttribute(.attachment) { (attachment: PillTextAttachment, range: NSRange, _) in
|
||||
if let displayText = attachment.data?.displayText,
|
||||
let userId = attachment.data?.matrixItemId,
|
||||
let permalink = MXTools.permalinkToUser(withUserId: userId) {
|
||||
let pillString: String
|
||||
switch mode {
|
||||
case .displayname:
|
||||
pillString = displayText
|
||||
case .identifier:
|
||||
pillString = userId
|
||||
case .markdown:
|
||||
pillString = "[\(displayText)](\(permalink))"
|
||||
}
|
||||
newAttr.replaceCharacters(in: range, with: pillString)
|
||||
guard let data = attachment.data else {
|
||||
return
|
||||
}
|
||||
|
||||
let pillString: String
|
||||
switch mode {
|
||||
case .displayname:
|
||||
pillString = data.displayText
|
||||
case .identifier:
|
||||
pillString = data.pillIdentifier
|
||||
case .markdown:
|
||||
pillString = data.markdown
|
||||
}
|
||||
|
||||
newAttr.replaceCharacters(in: range, with: pillString)
|
||||
}
|
||||
|
||||
return newAttr.string
|
||||
}
|
||||
|
||||
|
||||
/// Creates an attributed string containing a pill for given room member.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -111,17 +117,13 @@ class PillsFormatter: NSObject {
|
||||
andUrl url: URL? = nil,
|
||||
isHighlighted: Bool,
|
||||
font: UIFont) -> NSAttributedString {
|
||||
|
||||
guard let attachment = PillTextAttachment(withRoomMember: roomMember, isHighlighted: isHighlighted, font: font) else {
|
||||
return NSAttributedString(string: roomMember.displayname)
|
||||
}
|
||||
let string = NSMutableAttributedString(attachment: attachment)
|
||||
string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length))
|
||||
if let url = url {
|
||||
string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length))
|
||||
}
|
||||
return string
|
||||
return attributedStringWithAttachment(attachment, link: url, font: font)
|
||||
}
|
||||
|
||||
|
||||
/// Update alpha of all `PillTextAttachment` contained in given attributed string.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -140,44 +142,37 @@ class PillsFormatter: NSObject {
|
||||
/// - roomState: room state for refresh, should be the latest available
|
||||
static func refreshPills(in attributedString: NSAttributedString, with roomState: MXRoomState) {
|
||||
attributedString.vc_enumerateAttribute(.attachment) { (pill: PillTextAttachment, range: NSRange, _) in
|
||||
guard let userId = pill.data?.matrixItemId,
|
||||
let roomMember = roomState.members.member(withUserId: userId) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch pill.data?.pillType {
|
||||
case .user(let userId):
|
||||
guard let roomMember = roomState.members.member(withUserId: userId) else {
|
||||
return
|
||||
}
|
||||
|
||||
pill.data?.displayName = roomMember.displayname
|
||||
pill.data?.avatarUrl = roomMember.avatarUrl
|
||||
pill.data?.items = [
|
||||
.avatar(url: roomMember.avatarUrl,
|
||||
string: roomMember.displayname,
|
||||
matrixId: roomMember.userId),
|
||||
.text(roomMember.displayname)
|
||||
]
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
@available (iOS 15.0, *)
|
||||
private extension PillsFormatter {
|
||||
/// Extract user id from given permalink
|
||||
/// - Parameter permalink: the permalink
|
||||
/// - Returns: userId, if any
|
||||
static func userIdFromPermalink(_ permalink: String) -> String? {
|
||||
let baseUrl: String
|
||||
// BWI: Take the permalinkbase url from our settings class
|
||||
if let clientBaseUrl = AppConfigService.shared.permalinkUrl() {
|
||||
baseUrl = String(format: "%@/#/user/", clientBaseUrl)
|
||||
} else {
|
||||
baseUrl = String(format: "%@/#/", kMXMatrixDotToUrl)
|
||||
extension PillsFormatter {
|
||||
|
||||
static func attributedStringWithAttachment(_ attachment: PillTextAttachment, link: URL?, font: UIFont) -> NSAttributedString {
|
||||
let string = NSMutableAttributedString(attachment: attachment)
|
||||
string.addAttribute(.font, value: font, range: .init(location: 0, length: string.length))
|
||||
if let url = link {
|
||||
string.addAttribute(.link, value: url, range: .init(location: 0, length: string.length))
|
||||
}
|
||||
return permalink.starts(with: baseUrl) ? String(permalink.dropFirst(baseUrl.count)) : nil
|
||||
}
|
||||
|
||||
/// Retrieve the latest available `MXRoomMember` from given data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userId: the id of the user
|
||||
/// - roomState: room state for message
|
||||
/// - latestRoomState: latest room state of the room containing this message
|
||||
/// - Returns: the room member, if available
|
||||
static func roomMember(withUserId userId: String,
|
||||
roomState: MXRoomState,
|
||||
andLatestRoomState latestRoomState: MXRoomState?) -> MXRoomMember? {
|
||||
return latestRoomState?.members.member(withUserId: userId) ?? roomState.members.member(withUserId: userId)
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ final class QRCodeReaderView: UIView {
|
||||
extension QRCodeReaderView: ZXCaptureDelegate {
|
||||
|
||||
func captureCameraIsReady(_ capture: ZXCapture!) {
|
||||
isScanning = true
|
||||
startScanning()
|
||||
}
|
||||
|
||||
func captureResult(_ capture: ZXCapture!, result: ZXResult!) {
|
||||
|
||||
@@ -33,6 +33,13 @@ enum RendezvousChannelAlgorithm: String {
|
||||
case ECDH_V2 = "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256"
|
||||
}
|
||||
|
||||
/// Algorithm name as per MSC3906
|
||||
enum RendezvousFlow: String {
|
||||
/// The v1 value never actually appears in JSON
|
||||
case SETUP_ADDITIONAL_DEVICE_V1 = "org.matrix.msc3906.v1"
|
||||
case SETUP_ADDITIONAL_DEVICE_V2 = "org.matrix.msc3906.setup.additional_device.v2"
|
||||
}
|
||||
|
||||
/// Allows communication through a secure channel. Based on MSC3886 and MSC3903
|
||||
@MainActor
|
||||
class RendezvousService {
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
|
||||
#import "MXKPreviewViewController.h"
|
||||
|
||||
// Constant used to determine whether an event is visible at the bottom of the tableview, based on its visible height
|
||||
static const CGFloat kCellVisibilityMinimumHeight = 8.0;
|
||||
|
||||
@interface MXKRoomViewController () <MXKPreviewViewControllerDelegate>
|
||||
{
|
||||
/**
|
||||
@@ -2419,7 +2422,7 @@
|
||||
contentBottomOffsetY = _bubblesTableView.contentSize.height;
|
||||
}
|
||||
// Be a bit less retrictive, consider visible an event at the bottom even if is partially hidden.
|
||||
contentBottomOffsetY += 8;
|
||||
contentBottomOffsetY += kCellVisibilityMinimumHeight;
|
||||
|
||||
// Reset the current event id
|
||||
currentEventIdAtTableBottom = nil;
|
||||
@@ -2489,24 +2492,26 @@
|
||||
if (acknowledge && self.isEventsAcknowledgementEnabled)
|
||||
{
|
||||
// Indicate to the homeserver that the user has read this event.
|
||||
|
||||
// Check whether the read marker must be updated.
|
||||
BOOL updateReadMarker = _updateRoomReadMarker;
|
||||
if (updateReadMarker && roomDataSource.room.accountData.readMarkerEventId)
|
||||
{
|
||||
MXEvent *currentReadMarkerEvent = [roomDataSource.mxSession.store eventWithEventId:roomDataSource.room.accountData.readMarkerEventId inRoom:roomDataSource.roomId];
|
||||
if (!currentReadMarkerEvent)
|
||||
{
|
||||
currentReadMarkerEvent = [roomDataSource eventWithEventId:roomDataSource.room.accountData.readMarkerEventId];
|
||||
}
|
||||
|
||||
// Update the read marker only if the current event is available, and the new event is posterior to it.
|
||||
updateReadMarker = (currentReadMarkerEvent && (currentReadMarkerEvent.originServerTs <= component.event.originServerTs));
|
||||
}
|
||||
|
||||
if (self.navigationController.viewControllers.lastObject == self)
|
||||
{
|
||||
[roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:updateReadMarker];
|
||||
// Check if the selected event is eligible to be the new read marker position too
|
||||
if (!bubbleData.collapsed && [self eligibleForReadMarkerUpdate:component.event])
|
||||
{
|
||||
BOOL updateRoomReadMarker = _updateRoomReadMarker && [self isEventPosteriorToCurrentReadMarker:component.event];
|
||||
// Acknowledge this event and update the read marker if needed
|
||||
[roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:updateRoomReadMarker];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Acknowledge only this event. The read marker is handled separately
|
||||
[roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:NO];
|
||||
|
||||
if (_updateRoomReadMarker)
|
||||
{
|
||||
// Try to find the best event for the new read marker position
|
||||
[self updateReadMarkerEvent];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -2517,6 +2522,118 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)eligibleForReadMarkerUpdate:(MXEvent *)event {
|
||||
// Prevent the readmarker to be placed on a relatesTo or a redaction event
|
||||
if (event.relatesTo || event.redacts)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isEventPosteriorToCurrentReadMarker:(MXEvent *)event {
|
||||
if (roomDataSource.room.accountData.readMarkerEventId)
|
||||
{
|
||||
MXEvent *currentReadMarkerEvent = [roomDataSource.mxSession.store eventWithEventId:roomDataSource.room.accountData.readMarkerEventId inRoom:roomDataSource.roomId];
|
||||
if (!currentReadMarkerEvent)
|
||||
{
|
||||
currentReadMarkerEvent = [roomDataSource eventWithEventId:roomDataSource.room.accountData.readMarkerEventId];
|
||||
}
|
||||
|
||||
// Update the read marker only if the current event is available, and the new event is posterior to it.
|
||||
return currentReadMarkerEvent && (currentReadMarkerEvent.originServerTs <= event.originServerTs);
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
/// Try to update the read marker by looking for an eligible event displayed at the bottom of the tableview
|
||||
- (void)updateReadMarkerEvent
|
||||
{
|
||||
// Compute the content offset corresponding to the line displayed at the table bottom (just above the toolbar).
|
||||
CGFloat contentBottomOffsetY = _bubblesTableView.contentOffset.y + (_bubblesTableView.frame.size.height - _bubblesTableView.adjustedContentInset.bottom);
|
||||
if (contentBottomOffsetY > _bubblesTableView.contentSize.height)
|
||||
{
|
||||
contentBottomOffsetY = _bubblesTableView.contentSize.height;
|
||||
}
|
||||
// Be a bit less retrictive, consider visible an event at the bottom even if is partially hidden.
|
||||
contentBottomOffsetY += kCellVisibilityMinimumHeight;
|
||||
|
||||
// Consider the visible cells (starting by those displayed at the bottom)
|
||||
NSArray *visibleCells = [_bubblesTableView visibleCells];
|
||||
NSInteger index = visibleCells.count;
|
||||
UITableViewCell *cell;
|
||||
while (index--)
|
||||
{
|
||||
cell = visibleCells[index];
|
||||
|
||||
// Check whether the cell is actually visible
|
||||
if (!cell || cell.frame.origin.y > contentBottomOffsetY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (![cell isKindOfClass:MXKRoomBubbleTableViewCell.class])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
|
||||
MXKRoomBubbleCellData *bubbleData = roomBubbleTableViewCell.bubbleData;
|
||||
if (!bubbleData)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prevent to place the read marker on a collapsed cell
|
||||
if (bubbleData.collapsed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check which bubble component is displayed at the bottom.
|
||||
// For that update each component position.
|
||||
[bubbleData prepareBubbleComponentsPosition];
|
||||
|
||||
NSArray *bubbleComponents = bubbleData.bubbleComponents;
|
||||
NSInteger componentIndex = bubbleComponents.count;
|
||||
|
||||
CGFloat bottomPositionY = cell.frame.size.height;
|
||||
|
||||
MXKRoomBubbleComponent *component;
|
||||
|
||||
while (componentIndex --)
|
||||
{
|
||||
component = bubbleComponents[componentIndex];
|
||||
|
||||
// Prevent the read marker to be placed on an unsupported event (e.g. redactions, reactions, ...)
|
||||
if (![self eligibleForReadMarkerUpdate:component.event])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check whether the bottom part of the component is visible.
|
||||
CGFloat pos = cell.frame.origin.y + bottomPositionY;
|
||||
if (pos <= contentBottomOffsetY)
|
||||
{
|
||||
// We found the component
|
||||
// Check whether the read marker must be updated.
|
||||
if ([self isEventPosteriorToCurrentReadMarker:component.event])
|
||||
{
|
||||
// Move the read marker to this event
|
||||
[roomDataSource.room moveReadMarkerToEventId:component.event.eventId];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the bottom position for the next component
|
||||
bottomPositionY = roomBubbleTableViewCell.msgTextViewTopConstraint.constant + component.position.y;
|
||||
}
|
||||
|
||||
// else we consider the previous cell.
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - MXKDataSourceDelegate
|
||||
|
||||
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
|
||||
|
||||
@@ -1109,7 +1109,8 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
_voiceMessageController.roomId = dataSource.roomId;
|
||||
|
||||
_userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager
|
||||
room:dataSource.room];
|
||||
room:dataSource.room
|
||||
userID:self.roomDataSource.mxSession.myUserId];
|
||||
_userSuggestionCoordinator.delegate = self;
|
||||
|
||||
[self setupUserSuggestionViewIfNeeded];
|
||||
@@ -5377,7 +5378,7 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
[self dismissKeyboard];
|
||||
NSString *eventId = self.roomDataSource.room.accountData.readMarkerEventId;
|
||||
NSString *threadId = self.roomDataSource.threadId;
|
||||
[self reloadRoomWihtEventId:eventId threadId:threadId];
|
||||
[self reloadRoomWihtEventId:eventId threadId:threadId forceUpdateRoomMarker:YES];
|
||||
}
|
||||
else if (sender == self.resetReadMarkerButton)
|
||||
{
|
||||
@@ -6676,8 +6677,12 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
// Check whether the read marker exists and has not been rendered yet.
|
||||
if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking && self.roomDataSource.showReadMarker && self.roomDataSource.room.accountData.readMarkerEventId)
|
||||
{
|
||||
UITableViewCell *cell = [self.bubblesTableView visibleCells].firstObject;
|
||||
if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class])
|
||||
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
|
||||
return [evaluatedObject isKindOfClass:MXKRoomBubbleTableViewCell.class];
|
||||
}];
|
||||
NSArray *visibleCells = [[self.bubblesTableView visibleCells] filteredArrayUsingPredicate:predicate];
|
||||
UITableViewCell *cell = visibleCells.firstObject;
|
||||
if (cell)
|
||||
{
|
||||
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
|
||||
// Check whether the read marker is inside the first displayed cell.
|
||||
@@ -6704,6 +6709,9 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
else
|
||||
{
|
||||
self.jumpToLastUnreadBannerContainer.hidden = YES;
|
||||
|
||||
// Force the read marker position in order to not depend on the read marker animation (https://github.com/vector-im/element-ios/issues/7420)
|
||||
self.updateRoomReadMarker = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8000,15 +8008,17 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
[[AppDelegate theDelegate] showRoomWithParameters:parameters];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinator
|
||||
viewEventInTimeline:(MXEvent *)event
|
||||
{
|
||||
[self.navigationController popToViewController:self animated:true];
|
||||
[self reloadRoomWihtEventId:event.eventId threadId:event.threadId];
|
||||
[self reloadRoomWihtEventId:event.eventId threadId:event.threadId forceUpdateRoomMarker:NO];
|
||||
}
|
||||
|
||||
-(void)reloadRoomWihtEventId:(NSString *)eventId
|
||||
threadId:(NSString *)threadId
|
||||
forceUpdateRoomMarker:(BOOL)forceUpdateRoomMarker
|
||||
{
|
||||
// Jump to the last unread event by using a temporary room data source initialized with the last unread event id.
|
||||
MXWeakify(self);
|
||||
@@ -8027,6 +8037,9 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
|
||||
// Give the data source ownership to the room view controller.
|
||||
self.hasRoomDataSourceOwnership = YES;
|
||||
|
||||
// Force the read marker update if needed (e.g if we jumped on the last unread message using the banner).
|
||||
self.updateRoomReadMarker |= forceUpdateRoomMarker;
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -8154,6 +8167,19 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator
|
||||
didRequestMentionForMember:(MXRoomMember *)member
|
||||
textTrigger:(NSString *)textTrigger
|
||||
{
|
||||
[self removeTriggerTextFromComposer:textTrigger];
|
||||
[self mention:member];
|
||||
}
|
||||
|
||||
- (void)userSuggestionCoordinatorBridgeDidRequestMentionForRoom:(UserSuggestionCoordinatorBridge *)coordinator
|
||||
textTrigger:(NSString *)textTrigger
|
||||
{
|
||||
[self removeTriggerTextFromComposer:textTrigger];
|
||||
[self.inputToolbarView pasteText:[UserSuggestionID.room stringByAppendingString:@" "]];
|
||||
}
|
||||
|
||||
- (void)removeTriggerTextFromComposer:(NSString *)textTrigger
|
||||
{
|
||||
RoomInputToolbarView *toolbar = (RoomInputToolbarView *)self.inputToolbarView;
|
||||
if (toolbar && textTrigger.length) {
|
||||
@@ -8164,8 +8190,6 @@ static CGSize kThreadListBarButtonItemImageSize;
|
||||
range:NSMakeRange(0, attributedTextMessage.length)];
|
||||
[toolbar setAttributedTextMessage:attributedTextMessage];
|
||||
}
|
||||
|
||||
[self mention:member];
|
||||
}
|
||||
|
||||
- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
#ifndef EventEncryptionDecoration_h
|
||||
#define EventEncryptionDecoration_h
|
||||
|
||||
/**
|
||||
Decoration used alongside encrypted events
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, EventEncryptionDecoration)
|
||||
{
|
||||
EventEncryptionDecorationNone,
|
||||
EventEncryptionDecorationUnsafeKey,
|
||||
EventEncryptionDecorationDecryptionError,
|
||||
EventEncryptionDecorationNotEncrypted,
|
||||
EventEncryptionDecorationUntrustedDevice
|
||||
EventEncryptionDecorationGrey,
|
||||
EventEncryptionDecorationRed
|
||||
};
|
||||
|
||||
|
||||
#endif /* EventEncryptionDecoration_h */
|
||||
|
||||
@@ -27,11 +27,9 @@ NSString *const kRoomEncryptedDataBubbleCellTapOnEncryptionIcon = @"kRoomEncrypt
|
||||
switch (bubbleComponent.encryptionDecoration) {
|
||||
case EventEncryptionDecorationNone:
|
||||
return nil;
|
||||
case EventEncryptionDecorationUnsafeKey:
|
||||
case EventEncryptionDecorationGrey:
|
||||
return AssetImages.encryptionUntrusted.image;
|
||||
case EventEncryptionDecorationDecryptionError:
|
||||
case EventEncryptionDecorationNotEncrypted:
|
||||
case EventEncryptionDecorationUntrustedDevice:
|
||||
case EventEncryptionDecorationRed:
|
||||
return AssetImages.encryptionWarning.image;
|
||||
default:
|
||||
return nil;
|
||||
|
||||
+18
-15
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="KkK-aQ-7Ig">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="KkK-aQ-7Ig">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -11,7 +11,7 @@
|
||||
<!--Secrets Recovery With Passphrase View Controller-->
|
||||
<scene sceneID="r1I-YV-Fog">
|
||||
<objects>
|
||||
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="KkK-aQ-7Ig" customClass="SecretsRecoveryWithPassphraseViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="KkK-aQ-7Ig" customClass="SecretsRecoveryWithPassphraseViewController" customModule="Element" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8SG-gc-Id7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@@ -40,7 +40,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uly-5I-NIc">
|
||||
<rect key="frame" x="0.0" y="207" width="375" height="50"/>
|
||||
<rect key="frame" x="20" y="207" width="335" height="50"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="enter" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="I7Q-Tb-YGd">
|
||||
<rect key="frame" x="20" y="10" width="38" height="30"/>
|
||||
@@ -49,7 +49,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Enter passphrase" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="rAd-wZ-jgA">
|
||||
<rect key="frame" x="78" y="0.0" width="243" height="50"/>
|
||||
<rect key="frame" x="78" y="0.0" width="203" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="iy4-UK-b6r"/>
|
||||
</constraints>
|
||||
@@ -60,7 +60,7 @@
|
||||
</connections>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ahr-Zq-UM4">
|
||||
<rect key="frame" x="321" y="3" width="44" height="44"/>
|
||||
<rect key="frame" x="281" y="3" width="44" height="44"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="44" id="2WM-IG-od1"/>
|
||||
<constraint firstAttribute="height" constant="44" id="6DP-64-vVH"/>
|
||||
@@ -87,7 +87,7 @@
|
||||
<constraint firstItem="rAd-wZ-jgA" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="I7Q-Tb-YGd" secondAttribute="trailing" constant="20" id="vyG-oS-use"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="a20-Ii-sAN">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="a20-Ii-sAN">
|
||||
<rect key="frame" x="0.0" y="273" width="375" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="9rW-yL-qS1"/>
|
||||
@@ -95,7 +95,7 @@
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<inset key="contentEdgeInsets" minX="20" minY="0.0" maxX="20" maxY="0.0"/>
|
||||
<state key="normal" title="Don’t know your recovery passphrase ? You can use your recovery key.">
|
||||
<color key="titleColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<color key="titleColor" systemColor="darkTextColor"/>
|
||||
</state>
|
||||
<state key="disabled">
|
||||
<color key="titleColor" red="0.47843137250000001" green="0.78823529410000004" blue="0.63137254899999995" alpha="0.5" colorSpace="calibratedRGB"/>
|
||||
@@ -107,7 +107,7 @@
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="zOv-dc-49b">
|
||||
<rect key="frame" x="0.0" y="353" width="375" height="50"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DpI-8g-yKB" customClass="RoundedButton" customModule="Riot" customModuleProvider="target">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DpI-8g-yKB" customClass="RoundedButton" customModule="Element" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="0.0" width="335" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="Ghb-Uq-q6w"/>
|
||||
@@ -133,7 +133,7 @@
|
||||
<constraint firstItem="DpI-8g-yKB" firstAttribute="width" secondItem="zOv-dc-49b" secondAttribute="width" priority="750" id="q7z-h9-oc7"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FsM-8o-pIc">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FsM-8o-pIc">
|
||||
<rect key="frame" x="0.0" y="419" width="375" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="Zb9-Il-WIS"/>
|
||||
@@ -141,7 +141,7 @@
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<inset key="contentEdgeInsets" minX="20" minY="0.0" maxX="20" maxY="0.0"/>
|
||||
<state key="normal" title="Forgot or lost all recovery options? Reset everything">
|
||||
<color key="titleColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<color key="titleColor" systemColor="darkTextColor"/>
|
||||
</state>
|
||||
<state key="disabled">
|
||||
<color key="titleColor" red="0.47843137250000001" green="0.78823529410000004" blue="0.63137254899999995" alpha="0.5" colorSpace="calibratedRGB"/>
|
||||
@@ -153,7 +153,7 @@
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="uly-5I-NIc" secondAttribute="trailing" id="6VB-MQ-hIp"/>
|
||||
<constraint firstAttribute="trailing" secondItem="uly-5I-NIc" secondAttribute="trailing" constant="20" id="6VB-MQ-hIp"/>
|
||||
<constraint firstItem="p2V-aL-g0y" firstAttribute="top" secondItem="hA4-wJ-xGz" secondAttribute="bottom" constant="30" id="6eX-cP-a3F"/>
|
||||
<constraint firstAttribute="trailing" secondItem="zOv-dc-49b" secondAttribute="trailing" id="9er-kg-arg"/>
|
||||
<constraint firstItem="FsM-8o-pIc" firstAttribute="top" secondItem="zOv-dc-49b" secondAttribute="bottom" constant="16" id="KfQ-rd-n2B"/>
|
||||
@@ -164,7 +164,7 @@
|
||||
<constraint firstItem="zOv-dc-49b" firstAttribute="top" secondItem="a20-Ii-sAN" secondAttribute="bottom" constant="30" id="b6e-I5-UmV"/>
|
||||
<constraint firstItem="zOv-dc-49b" firstAttribute="leading" secondItem="Gw9-uS-bGl" secondAttribute="leading" id="bdZ-LL-sEK"/>
|
||||
<constraint firstAttribute="bottom" secondItem="FsM-8o-pIc" secondAttribute="bottom" constant="20" id="cFV-lL-sr0"/>
|
||||
<constraint firstItem="uly-5I-NIc" firstAttribute="leading" secondItem="Gw9-uS-bGl" secondAttribute="leading" id="cbx-lF-FxP"/>
|
||||
<constraint firstItem="uly-5I-NIc" firstAttribute="leading" secondItem="Gw9-uS-bGl" secondAttribute="leading" constant="20" id="cbx-lF-FxP"/>
|
||||
<constraint firstItem="FsM-8o-pIc" firstAttribute="leading" secondItem="Gw9-uS-bGl" secondAttribute="leading" id="dlb-Mp-dHQ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="p2V-aL-g0y" secondAttribute="trailing" constant="20" id="ebM-3Y-G7G"/>
|
||||
<constraint firstAttribute="trailing" secondItem="FsM-8o-pIc" secondAttribute="trailing" id="jtO-Hw-15P"/>
|
||||
@@ -196,6 +196,7 @@
|
||||
</constraints>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="9Os-Vv-Xnb"/>
|
||||
<color key="backgroundColor" red="0.94509803920000002" green="0.96078431369999995" blue="0.97254901959999995" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="FYl-Bb-Kpe" firstAttribute="leading" secondItem="9Os-Vv-Xnb" secondAttribute="leading" id="GOs-MF-rUL"/>
|
||||
@@ -203,7 +204,6 @@
|
||||
<constraint firstItem="9Os-Vv-Xnb" firstAttribute="trailing" secondItem="FYl-Bb-Kpe" secondAttribute="trailing" id="SYj-74-mpC"/>
|
||||
<constraint firstItem="9Os-Vv-Xnb" firstAttribute="top" secondItem="FYl-Bb-Kpe" secondAttribute="top" id="dg5-In-5U7"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="9Os-Vv-Xnb"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="informationLabel" destination="p2V-aL-g0y" id="f0u-LY-N8z"/>
|
||||
@@ -225,6 +225,9 @@
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="reveal_password_button" width="24" height="18"/>
|
||||
<image name="secrets_recovery_passphrase" width="44" height="48"/>
|
||||
<image name="secrets_recovery_passphrase" width="256" height="256"/>
|
||||
<systemColor name="darkTextColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
+23
-1
@@ -78,10 +78,20 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController {
|
||||
|
||||
self.registerThemeServiceDidChangeThemeNotification()
|
||||
self.update(theme: self.theme)
|
||||
self.passphraseTextFieldBackgroundView.layer.cornerRadius = 8.0
|
||||
self.passphraseTextFieldBackgroundView.layer.borderWidth = 1.5
|
||||
self.passphraseTextFieldBackgroundView.layer.borderColor = ThemeService.shared().theme.textQuinaryColor.cgColor
|
||||
self.passphraseTextField.delegate = self
|
||||
|
||||
self.viewModel.viewDelegate = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.passphraseTextFieldBackgroundView.layer.borderWidth = 1.5
|
||||
self.passphraseTextFieldBackgroundView.layer.borderColor = ThemeService.shared().theme.textQuinaryColor.cgColor
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return self.theme.statusBarStyle
|
||||
}
|
||||
@@ -105,7 +115,7 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController {
|
||||
|
||||
let visibilityImage = Asset.Images.revealPasswordButton.image.withRenderingMode(.alwaysTemplate)
|
||||
self.passphraseVisibilityButton.setImage(visibilityImage, for: .normal)
|
||||
|
||||
self.passphraseVisibilityButton.accessibilityLabel = BWIL10n.textfieldRevealSecret
|
||||
let informationText: String
|
||||
|
||||
switch self.viewModel.recoveryGoal {
|
||||
@@ -194,8 +204,10 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController {
|
||||
private func updateShowHidePassphraseButton() {
|
||||
if self.passphraseTextField.isSecureTextEntry {
|
||||
passphraseVisibilityButton.setImage(Asset.Images.revealPasswordButton.image, for: .normal)
|
||||
passphraseVisibilityButton.accessibilityLabel = BWIL10n.textfieldRevealSecret
|
||||
} else {
|
||||
passphraseVisibilityButton.setImage(Asset.Images.hidePasswordButton.image, for: .normal)
|
||||
passphraseVisibilityButton.accessibilityLabel = BWIL10n.textfieldHideSecret
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +282,16 @@ extension SecretsRecoveryWithPassphraseViewController: UITextFieldDelegate {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
passphraseTextFieldBackgroundView.layer.borderWidth = 2.0
|
||||
passphraseTextFieldBackgroundView.layer.borderColor = ThemeService.shared().theme.tintColor.cgColor
|
||||
}
|
||||
|
||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
passphraseTextFieldBackgroundView.layer.borderWidth = 1.5
|
||||
passphraseTextFieldBackgroundView.layer.borderColor = ThemeService.shared().theme.textQuinaryColor.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SecretsRecoveryWithPassphraseViewModelViewDelegate
|
||||
|
||||
@@ -328,6 +328,7 @@
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="bottomView" destination="a0M-Zw-56s" id="nlL-WO-RWV"/>
|
||||
<outlet property="digitButtonReset" destination="yUt-tN-2gL" id="cRd-ey-eJZ"/>
|
||||
<outlet property="digitsStackView" destination="5Ek-SS-9WZ" id="1TV-zd-a0a"/>
|
||||
<outlet property="explanatoryLabel" destination="vP5-71-7ne" id="Skx-tQ-sV1"/>
|
||||
<outlet property="forgotPinButton" destination="TMg-AX-OiC" id="0xJ-0V-oGD"/>
|
||||
|
||||
@@ -44,7 +44,7 @@ final class EnterPinCodeViewController: UIViewController {
|
||||
@IBOutlet private weak var explanatoryLabel: UILabel!
|
||||
@IBOutlet private weak var forgotPinButton: UIButton!
|
||||
@IBOutlet private weak var bottomView: UIView!
|
||||
|
||||
@IBOutlet private weak var digitButtonReset: UIButton!
|
||||
// MARK: Private
|
||||
|
||||
private var viewModel: EnterPinCodeViewModelType!
|
||||
@@ -87,6 +87,8 @@ final class EnterPinCodeViewController: UIViewController {
|
||||
if BWIBuildSettings.shared.showBUMLottieAnimation {
|
||||
inactiveLogoImageView.isHidden = true
|
||||
}
|
||||
// BWI: accessibility description
|
||||
digitButtonReset.vc_setupAccessibilityTraitsButton(withTitle: BWIL10n.pinProtectionResetButtonAccessibilityLabel, hint: BWIL10n.pinProtectionResetButtonAccessibilityHint, isEnabled: true)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
||||
@@ -328,7 +328,7 @@ TableViewSectionsDelegate>
|
||||
[sections addObject:pinCodeSection];
|
||||
|
||||
// BWI: if tracking is enabled show switch to turn it on/off
|
||||
if ([BWIAnalytics.sharedTracker isEnabled]) {
|
||||
if (BWIBuildSettings.shared.bwiMatomoEnabled) {
|
||||
Section *analyticsSection = [Section sectionWithTag:SECTION_ANALYTICS];
|
||||
[analyticsSection addRowWithTag:ANALYTICS_ENABLED];
|
||||
analyticsSection.headerTitle = BWIL10n.bwiSettingsAnalyticsSectionHeader;
|
||||
@@ -337,7 +337,7 @@ TableViewSectionsDelegate>
|
||||
}
|
||||
|
||||
// Crypto sessions section
|
||||
if (RiotSettings.shared.settingsSecurityScreenShowSessions && !RiotSettings.shared.enableNewSessionManager)
|
||||
if (RiotSettings.shared.settingsSecurityScreenShowSessions && !RiotSettings.shared.enableNewSessionManager && !BWIBuildSettings.shared.enableNewSessionManagerByDefault)
|
||||
{
|
||||
Section *sessionsSection = [Section sectionWithTag:SECTION_CRYPTO_SESSIONS];
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ typedef NS_ENUM(NSUInteger, ABOUT)
|
||||
ABOUT_ELEMENT_VERSION_INDEX,
|
||||
ABOUT_COPYRIGHT_INDEX,
|
||||
ABOUT_TERM_CONDITIONS_INDEX,
|
||||
ABOUT_ACCEPTABLE_USE_INDEX,
|
||||
ABOUT_PRIVACY_INDEX,
|
||||
ABOUT_THIRD_PARTY_INDEX,
|
||||
ABOUT_SUPPORT_INDEX,
|
||||
@@ -502,7 +503,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
||||
Section *sectionSecurity = [Section sectionWithTag:SECTION_TAG_SECURITY];
|
||||
[sectionSecurity addRowWithTag:SECURITY_BUTTON_INDEX];
|
||||
|
||||
if (RiotSettings.shared.enableNewSessionManager || BWIBuildSettings.shared.showSessionManager)
|
||||
if (RiotSettings.shared.enableNewSessionManager || BWIBuildSettings.shared.enableNewSessionManagerByDefault)
|
||||
{
|
||||
// NOTE: Add device manager entry point in the security section atm for debug purpose
|
||||
[sectionSecurity addRowWithTag:DEVICE_MANAGER_INDEX];
|
||||
@@ -719,7 +720,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
||||
|
||||
if (BWIBuildSettings.shared.applicationTermsConditionsUrlString.length)
|
||||
{
|
||||
[sectionAbout addRowWithTag:ABOUT_TERM_CONDITIONS_INDEX];
|
||||
[sectionAbout addRowWithTag:ABOUT_ACCEPTABLE_USE_INDEX];
|
||||
}
|
||||
if (BWIBuildSettings.shared.applicationPrivacyPolicyUrlString.length)
|
||||
{
|
||||
@@ -3308,11 +3309,11 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
||||
|
||||
[self pushViewController:webViewViewController];
|
||||
}
|
||||
else if (row == ABOUT_TERM_CONDITIONS_INDEX)
|
||||
else if (row == ABOUT_ACCEPTABLE_USE_INDEX)
|
||||
{
|
||||
WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BuildSettings.applicationTermsConditionsUrlString];
|
||||
WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BuildSettings.applicationAcceptableUsePolicyUrlString];
|
||||
|
||||
webViewViewController.title = [VectorL10n settingsTermConditions];
|
||||
webViewViewController.title = [VectorL10n settingsAcceptableUse];
|
||||
[webViewViewController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever];
|
||||
|
||||
[self pushViewController:webViewViewController];
|
||||
|
||||
@@ -57,7 +57,8 @@ final class SpaceAvatarView: AvatarView, NibOwnerLoadable {
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.avatarImageView.layer.cornerRadius = Constants.cornerRadius
|
||||
// Ensure we keep a rounded corner if the width is less than 2 * Constants.cornerRadius
|
||||
self.avatarImageView.layer.cornerRadius = max(2.0, min(self.avatarImageView.bounds.width / 4, Constants.cornerRadius))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
@@ -23,6 +23,7 @@ final class UserAvatarView: AvatarView {
|
||||
|
||||
private func commonInit() {
|
||||
let avatarImageView = MXKImageView()
|
||||
avatarImageView.frame = self.frame
|
||||
self.vc_addSubViewMatchingParent(avatarImageView)
|
||||
self.avatarImageView = avatarImageView
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ targets:
|
||||
- path: ../bwi
|
||||
excludes:
|
||||
- "Tests"
|
||||
- "ServerURLs/serverurls.json"
|
||||
- path: ../RiotShareExtension/Shared
|
||||
- path: Modules/MatrixKit
|
||||
excludes:
|
||||
|
||||
@@ -52,6 +52,7 @@ targets:
|
||||
- path: ../bwi/NotificationTimes/Date+fromHourMinute.swift
|
||||
- path: ../bwi/MatomoAnalytics/BWIAnalytics.swift
|
||||
- path: ../bwi/MatomoAnalytics/BWIAnalyticsAccountDataService.swift
|
||||
- path: ../bwi/MatomoAnalytics/AnalyticsConfiguration.swift
|
||||
- path: ../bwi/MatomoAnalytics/E2EEError.swift
|
||||
- path: ../Riot/Managers/Settings/RiotSettings.swift
|
||||
- path: ../Config/BuildSettings.swift
|
||||
|
||||
@@ -18,6 +18,7 @@ import Foundation
|
||||
|
||||
struct QRLoginCode: Codable {
|
||||
let rendezvous: RendezvousDetails
|
||||
let flow: String?
|
||||
let intent: String
|
||||
}
|
||||
|
||||
@@ -42,7 +43,8 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
|
||||
var intent: Intent?
|
||||
var outcome: Outcome?
|
||||
|
||||
var reason: FailureReason?
|
||||
|
||||
// swiftformat:disable:next redundantBackticks
|
||||
var protocols: [`Protocol`]?
|
||||
|
||||
@@ -64,6 +66,7 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
case type
|
||||
case intent
|
||||
case outcome
|
||||
case reason
|
||||
case homeserver
|
||||
case user
|
||||
case protocols
|
||||
@@ -77,9 +80,18 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
}
|
||||
|
||||
enum `Type`: String, Codable {
|
||||
case loginStart = "m.login.start"
|
||||
case loginProgress = "m.login.progress"
|
||||
/**
|
||||
This is only used in MSC3906 v1 and will be removed
|
||||
*/
|
||||
case loginFinish = "m.login.finish"
|
||||
case loginFailure = "m.login.failure"
|
||||
case loginProtocol = "m.login.protocol"
|
||||
case loginProtocols = "m.login.protocols"
|
||||
case loginApproved = "m.login.approved"
|
||||
case loginDeclined = "m.login.declined"
|
||||
case loginSuccess = "m.login.success"
|
||||
case loginVerified = "m.login.verified"
|
||||
}
|
||||
|
||||
enum Intent: String, Codable {
|
||||
@@ -87,6 +99,9 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
case loginReciprocate = "login.reciprocate"
|
||||
}
|
||||
|
||||
/**
|
||||
This is only used in MSC306 v1 and will be removed
|
||||
*/
|
||||
enum Outcome: String, Codable {
|
||||
case success
|
||||
case declined
|
||||
@@ -97,4 +112,11 @@ struct QRLoginRendezvousPayload: Codable {
|
||||
enum `Protocol`: String, Codable {
|
||||
case loginToken = "org.matrix.msc3906.login_token"
|
||||
}
|
||||
|
||||
enum FailureReason: String, Codable {
|
||||
case cancelled
|
||||
case unsupported
|
||||
case e2eeSecurityError = "e2ee_security_error"
|
||||
case incompatibleIntent = "incompatible_intent"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-6
@@ -180,6 +180,12 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
guard let flow = code.flow != nil ? RendezvousFlow(rawValue: code.flow!) : .SETUP_ADDITIONAL_DEVICE_V1 else {
|
||||
MXLog.error("[QRLoginService] Unsupported flow")
|
||||
state = .failed(error: .deviceNotSupported)
|
||||
return
|
||||
}
|
||||
|
||||
// so, this is of an expected algorithm so any bad data can be considered an invalid QR code
|
||||
guard code.intent == QRLoginRendezvousPayload.Intent.loginReciprocate.rawValue,
|
||||
let uri = code.rendezvous.transport?.uri,
|
||||
@@ -223,7 +229,10 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
|
||||
}
|
||||
|
||||
MXLog.debug("[QRLoginService] Request login with `login_token`")
|
||||
guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginProgress, protocol: .loginToken)),
|
||||
let protocolPayload = flow == .SETUP_ADDITIONAL_DEVICE_V1
|
||||
? QRLoginRendezvousPayload(type: .loginProgress, protocol: .loginToken)
|
||||
: QRLoginRendezvousPayload(type: .loginProtocol, protocol: .loginToken)
|
||||
guard let requestData = try? JSONEncoder().encode(protocolPayload),
|
||||
case .success = await rendezvousService.send(data: requestData) else {
|
||||
MXLog.error("[QRLoginService] Failed sending continue with `login_token` request")
|
||||
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
||||
@@ -282,10 +291,11 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
|
||||
}
|
||||
|
||||
MXLog.debug("[QRLoginService] Session created, sending device details")
|
||||
guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginProgress,
|
||||
outcome: .success,
|
||||
deviceId: session.myDeviceId,
|
||||
deviceKey: session.crypto.deviceEd25519Key)),
|
||||
let successPayload = flow == .SETUP_ADDITIONAL_DEVICE_V1
|
||||
? QRLoginRendezvousPayload(type: .loginProgress, outcome: .success, deviceId: session.myDeviceId, deviceKey: session.crypto.deviceEd25519Key)
|
||||
: QRLoginRendezvousPayload(type: .loginSuccess, deviceId: session.myDeviceId, deviceKey: session.crypto.deviceEd25519Key)
|
||||
|
||||
guard let requestData = try? JSONEncoder().encode(successPayload),
|
||||
case .success = await rendezvousService.send(data: requestData) else {
|
||||
MXLog.error("[QRLoginService] Failed sending session details")
|
||||
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
||||
@@ -307,7 +317,7 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
|
||||
MXLog.debug("[QRLoginService] Wait for cross-signing details")
|
||||
guard case let .success(data) = await rendezvousService.receive(),
|
||||
let responsePayload = try? JSONDecoder().decode(QRLoginRendezvousPayload.self, from: data),
|
||||
responsePayload.outcome == .verified,
|
||||
flow == .SETUP_ADDITIONAL_DEVICE_V1 && responsePayload.outcome == .verified || responsePayload.type == .loginVerified,
|
||||
let verifiyingDeviceId = responsePayload.verifyingDeviceId,
|
||||
let verifyingDeviceKey = responsePayload.verifyingDeviceKey else {
|
||||
MXLog.error("[QRLoginService] Received invalid cross-signing details")
|
||||
|
||||
+5
-1
@@ -20,13 +20,16 @@ import SwiftUI
|
||||
|
||||
class MockQRLoginService: QRLoginServiceProtocol {
|
||||
private let mockCanDisplayQR: Bool
|
||||
private let mockFlow: String?
|
||||
|
||||
init(withState state: QRLoginServiceState = .initial,
|
||||
mode: QRLoginServiceMode = .notAuthenticated,
|
||||
canDisplayQR: Bool = true) {
|
||||
canDisplayQR: Bool = true,
|
||||
flow: String? = nil) {
|
||||
self.state = state
|
||||
self.mode = mode
|
||||
mockCanDisplayQR = canDisplayQR
|
||||
mockFlow = flow
|
||||
}
|
||||
|
||||
// MARK: - QRLoginServiceProtocol
|
||||
@@ -57,6 +60,7 @@ class MockQRLoginService: QRLoginServiceProtocol {
|
||||
uri: "https://matrix.org"),
|
||||
key: "some.public.key")
|
||||
return QRLoginCode(rendezvous: details,
|
||||
flow: mockFlow,
|
||||
intent: "login.start")
|
||||
}
|
||||
|
||||
|
||||
+18
-4
@@ -62,10 +62,7 @@ struct AuthenticationServerSelectionScreen: View {
|
||||
serverForm
|
||||
.alert(item: $viewModel.alertInfo) { $0.alert }
|
||||
.alert(isPresented: $showAlertForInvalidServer) {
|
||||
Alert(
|
||||
title: Text(BWIL10n.authenticationServerSelectionServerDeniedTitle),
|
||||
message: Text(BWIL10n.authenticationServerSelectionServerDeniedMessage),
|
||||
dismissButton: .default(Text(VectorL10n.ok)))
|
||||
invalidServerAlert()
|
||||
}
|
||||
if BWIBuildSettings.shared.authScreenShowTestServerOptions {
|
||||
serverSelectionButton
|
||||
@@ -242,6 +239,23 @@ struct AuthenticationServerSelectionScreen: View {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// bwi: show alert with advertizment link if URL is valid
|
||||
private func invalidServerAlert() -> Alert {
|
||||
if let url = URL(string: BWIBuildSettings.shared.bumAdvertizementURLString) {
|
||||
return Alert(
|
||||
title: Text(BWIL10n.authenticationServerSelectionServerDeniedTitle),
|
||||
message: Text(BWIL10n.authenticationServerSelectionServerDeniedMessage),
|
||||
primaryButton: .default(Text(BWIL10n.authenticationServerSelectionServerDeniedAdvertizementWebsiteButton), action: {UIApplication.shared.open(url)}),
|
||||
secondaryButton: .default(Text(VectorL10n.ok)))
|
||||
|
||||
} else {
|
||||
return Alert(
|
||||
title: Text(BWIL10n.authenticationServerSelectionServerDeniedTitle),
|
||||
message: Text(BWIL10n.authenticationServerSelectionServerDeniedMessage),
|
||||
dismissButton: .default(Text(VectorL10n.ok)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ struct BorderedInputFieldStyle: TextFieldStyle {
|
||||
if isError {
|
||||
return theme.colors.alert
|
||||
} else if isEditing {
|
||||
return BWIBuildSettings.shared.bwiEnableBuMUI ? Color(hex: 0x108194) : theme.colors.accent
|
||||
return BWIBuildSettings.shared.bwiEnableBuMUI ? Color(ThemeService.shared().theme.tintColor) : theme.colors.accent
|
||||
}
|
||||
return theme.colors.quinaryContent
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ struct InlineTextButton: View {
|
||||
var text: Text = .init(component.string)
|
||||
|
||||
if component.isTinted {
|
||||
text = text.foregroundColor(.accentColor.opacity(configuration.isPressed ? 0.2 : 1))
|
||||
text = text.foregroundColor(Color(ThemeService.shared().theme.tintColor).opacity(configuration.isPressed ? 0.2 : 1))
|
||||
}
|
||||
|
||||
return lastValue + text
|
||||
|
||||
@@ -46,6 +46,7 @@ struct PasswordButtonModifier: ViewModifier {
|
||||
.frame(width: iconSize, height: iconSize)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
}
|
||||
.accessibilityLabel(isSecureTextVisible ? BWIL10n.textfieldHideSecret : BWIL10n.textfieldRevealSecret)
|
||||
.padding(.top, alignment == .top ? 8 : 0)
|
||||
.padding(.bottom, alignment == .bottom ? 8 : 0)
|
||||
.padding(.trailing, 12)
|
||||
|
||||
@@ -78,7 +78,6 @@ struct RoundedBorderTextField: View {
|
||||
.frame(height: 30)
|
||||
.allowsHitTesting(isEnabled)
|
||||
.opacity(isEnabled ? 1 : 0.5)
|
||||
.accessibilityLabel(text.isEmpty ? placeHolder : "")
|
||||
}
|
||||
.padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: text.isEmpty ? 8 : 0))
|
||||
.background(RoundedRectangle(cornerRadius: 8).fill(theme.colors.background))
|
||||
|
||||
@@ -138,6 +138,11 @@ struct ThemableTextField: UIViewRepresentable {
|
||||
|
||||
@objc func textFieldEditingChanged(sender: UITextField) {
|
||||
parent.replaceText(with: sender.text ?? "")
|
||||
if let text = sender.text, !text.isEmpty {
|
||||
sender.accessibilityLabel = text
|
||||
} else {
|
||||
sender.accessibilityLabel = sender.placeholder ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
-6
@@ -21,12 +21,14 @@ import UIKit
|
||||
|
||||
protocol UserSuggestionCoordinatorDelegate: AnyObject {
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
|
||||
func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?)
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat)
|
||||
}
|
||||
|
||||
struct UserSuggestionCoordinatorParameters {
|
||||
let mediaManager: MXMediaManager
|
||||
let room: MXRoom
|
||||
let userID: String
|
||||
}
|
||||
|
||||
final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
@@ -56,7 +58,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
init(parameters: UserSuggestionCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room)
|
||||
roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room, userID: parameters.userID)
|
||||
userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider)
|
||||
|
||||
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService)
|
||||
@@ -73,6 +75,11 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
|
||||
switch result {
|
||||
case .selectedItemWithIdentifier(let identifier):
|
||||
if identifier == UserSuggestionID.room {
|
||||
self.delegate?.userSuggestionCoordinatorDidRequestMentionForRoom(self, textTrigger: self.userSuggestionService.currentTextTrigger)
|
||||
return
|
||||
}
|
||||
|
||||
guard let member = self.roomMemberProvider.roomMembers.filter({ $0.userId == identifier }).first else {
|
||||
return
|
||||
}
|
||||
@@ -130,29 +137,44 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
|
||||
private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol {
|
||||
private let room: MXRoom
|
||||
private let userID: String
|
||||
|
||||
var roomMembers: [MXRoomMember] = []
|
||||
var canMentionRoom = false
|
||||
|
||||
init(room: MXRoom) {
|
||||
init(room: MXRoom, userID: String) {
|
||||
self.room = room
|
||||
self.userID = userID
|
||||
updateWithPowerLevels()
|
||||
}
|
||||
|
||||
/// Gets the power levels for the room to update suggestions accordingly.
|
||||
func updateWithPowerLevels() {
|
||||
room.state { [weak self] state in
|
||||
guard let self, let powerLevels = state?.powerLevels else { return }
|
||||
let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID)
|
||||
let mentionRoomPowerLevel = powerLevels.minimumPowerLevel(forNotifications: kMXRoomPowerLevelNotificationsRoomKey,
|
||||
defaultPower: kMXRoomPowerLevelNotificationsRoomDefault)
|
||||
self.canMentionRoom = userPowerLevel >= mentionRoomPowerLevel
|
||||
}
|
||||
}
|
||||
|
||||
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) {
|
||||
room.members({ [weak self] roomMembers in
|
||||
room.members { [weak self] roomMembers in
|
||||
guard let self = self, let joinedMembers = roomMembers?.joinedMembers else {
|
||||
return
|
||||
}
|
||||
self.roomMembers = joinedMembers
|
||||
members(self.roomMembersToProviderMembers(joinedMembers))
|
||||
}, lazyLoadedMembers: { [weak self] lazyRoomMembers in
|
||||
} lazyLoadedMembers: { [weak self] lazyRoomMembers in
|
||||
guard let self = self, let joinedMembers = lazyRoomMembers?.joinedMembers else {
|
||||
return
|
||||
}
|
||||
self.roomMembers = joinedMembers
|
||||
members(self.roomMembersToProviderMembers(joinedMembers))
|
||||
}, failure: { error in
|
||||
} failure: { error in
|
||||
MXLog.error("[UserSuggestionCoordinatorRoomMemberProvider] Failed loading room", context: error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func roomMembersToProviderMembers(_ roomMembers: [MXRoomMember]) -> [RoomMembersProviderMember] {
|
||||
|
||||
+7
-2
@@ -19,6 +19,7 @@ import Foundation
|
||||
@objc
|
||||
protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject {
|
||||
func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
|
||||
func userSuggestionCoordinatorBridgeDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinatorBridge, textTrigger: String?)
|
||||
func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat)
|
||||
}
|
||||
|
||||
@@ -31,8 +32,8 @@ final class UserSuggestionCoordinatorBridge: NSObject {
|
||||
|
||||
weak var delegate: UserSuggestionCoordinatorBridgeDelegate?
|
||||
|
||||
init(mediaManager: MXMediaManager, room: MXRoom) {
|
||||
let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room)
|
||||
init(mediaManager: MXMediaManager, room: MXRoom, userID: String) {
|
||||
let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room, userID: userID)
|
||||
let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters)
|
||||
_userSuggestionCoordinator = userSuggestionCoordinator
|
||||
|
||||
@@ -54,6 +55,10 @@ extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate {
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) {
|
||||
delegate?.userSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger)
|
||||
}
|
||||
|
||||
func userSuggestionCoordinatorDidRequestMentionForRoom(_ coordinator: UserSuggestionCoordinator, textTrigger: String?) {
|
||||
delegate?.userSuggestionCoordinatorBridgeDidRequestMentionForRoom(self, textTrigger: textTrigger)
|
||||
}
|
||||
|
||||
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) {
|
||||
delegate?.userSuggestionCoordinatorBridge(self, didUpdateViewHeight: height)
|
||||
|
||||
@@ -23,7 +23,13 @@ struct RoomMembersProviderMember {
|
||||
var avatarUrl: String
|
||||
}
|
||||
|
||||
class UserSuggestionID: NSObject {
|
||||
/// A special case added for suggesting `@room` mentions.
|
||||
@objc static let room = "@room"
|
||||
}
|
||||
|
||||
protocol RoomMembersProviderProtocol {
|
||||
var canMentionRoom: Bool { get }
|
||||
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void)
|
||||
}
|
||||
|
||||
@@ -100,7 +106,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
self.suggestionItems = members.map { member in
|
||||
self.suggestionItems = members.withRoom(self.roomMemberProvider.canMentionRoom).map { member in
|
||||
UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl)
|
||||
}
|
||||
|
||||
@@ -113,3 +119,11 @@ class UserSuggestionService: UserSuggestionServiceProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == RoomMembersProviderMember {
|
||||
/// Returns the array with an additional member that represents an `@room` mention.
|
||||
func withRoom(_ canMentionRoom: Bool) -> Self {
|
||||
guard canMentionRoom else { return self }
|
||||
return self + [RoomMembersProviderMember(userId: UserSuggestionID.room, displayName: "Everyone", avatarUrl: "")]
|
||||
}
|
||||
}
|
||||
|
||||
+69
-45
@@ -20,87 +20,111 @@ import XCTest
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
class UserSuggestionServiceTests: XCTestCase {
|
||||
var service: UserSuggestionService?
|
||||
var service: UserSuggestionService!
|
||||
var canMentionRoom = false
|
||||
|
||||
override func setUp() {
|
||||
service = UserSuggestionService(roomMemberProvider: self, shouldDebounce: false)
|
||||
canMentionRoom = false
|
||||
}
|
||||
|
||||
func testAlice() {
|
||||
service?.processTextMessage("@Al")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@Al")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
|
||||
service?.processTextMessage("@al")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@al")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
|
||||
service?.processTextMessage("@ice")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@ice")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
|
||||
service?.processTextMessage("@Alice")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@Alice")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
|
||||
service?.processTextMessage("@alice:matrix.org")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
service.processTextMessage("@alice:matrix.org")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
}
|
||||
|
||||
func testBob() {
|
||||
service?.processTextMessage("@ob")
|
||||
assert(service?.items.value.first?.displayName == "Bob")
|
||||
service.processTextMessage("@ob")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Bob")
|
||||
|
||||
service?.processTextMessage("@ob:")
|
||||
assert(service?.items.value.first?.displayName == "Bob")
|
||||
service.processTextMessage("@ob:")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Bob")
|
||||
|
||||
service?.processTextMessage("@b:matrix")
|
||||
assert(service?.items.value.first?.displayName == "Bob")
|
||||
service.processTextMessage("@b:matrix")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Bob")
|
||||
}
|
||||
|
||||
func testBoth() {
|
||||
service?.processTextMessage("@:matrix")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
assert(service?.items.value.last?.displayName == "Bob")
|
||||
service.processTextMessage("@:matrix")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
XCTAssertEqual(service.items.value.last?.displayName, "Bob")
|
||||
|
||||
service?.processTextMessage("@.org")
|
||||
assert(service?.items.value.first?.displayName == "Alice")
|
||||
assert(service?.items.value.last?.displayName == "Bob")
|
||||
service.processTextMessage("@.org")
|
||||
XCTAssertEqual(service.items.value.first?.displayName, "Alice")
|
||||
XCTAssertEqual(service.items.value.last?.displayName, "Bob")
|
||||
}
|
||||
|
||||
func testEmptyResult() {
|
||||
service?.processTextMessage("Lorem ipsum idolor")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("Lorem ipsum idolor")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("@")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("@")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("@@")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("@@")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("alice@matrix.org")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("alice@matrix.org")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
}
|
||||
|
||||
func testStuff() {
|
||||
service?.processTextMessage("@@")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("@@")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
}
|
||||
|
||||
func testWhitespaces() {
|
||||
service?.processTextMessage("")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage(" ")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage(" ")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("\n")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("\n")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage(" \n ")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage(" \n ")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage("@A ")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage("@A ")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
|
||||
service?.processTextMessage(" @A ")
|
||||
assert(service?.items.value.count == 0)
|
||||
service.processTextMessage(" @A ")
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
}
|
||||
|
||||
func testRoomWithoutPower() {
|
||||
// Given a user without the power to mention a room.
|
||||
canMentionRoom = false
|
||||
|
||||
// Given a user without the power to mention a room.
|
||||
service.processTextMessage("@ro")
|
||||
|
||||
// Then the completion for a room mention should not be shown.
|
||||
XCTAssertTrue(service.items.value.isEmpty)
|
||||
}
|
||||
|
||||
func testRoomWithPower() {
|
||||
// Given a user without the power to mention a room.
|
||||
canMentionRoom = true
|
||||
|
||||
// Given a user without the power to mention a room.
|
||||
service.processTextMessage("@ro")
|
||||
|
||||
// Then the completion for a room mention should be shown.
|
||||
XCTAssertEqual(service.items.value.first?.userId, UserSuggestionID.room)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable {
|
||||
}
|
||||
|
||||
extension MockUserSuggestionScreenState: RoomMembersProviderProtocol {
|
||||
var canMentionRoom: Bool { false }
|
||||
|
||||
func fetchMembers(_ members: ([RoomMembersProviderMember]) -> Void) {
|
||||
if Self.members == nil {
|
||||
Self.members = generateUsersWithCount(10)
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ class MXNotificationSettingsService: NotificationSettingsServiceType {
|
||||
let rulesUpdated = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules))
|
||||
|
||||
// Set initial value of the content rules
|
||||
if let contentRules = session.notificationCenter.rules.global.content as? [MXPushRule] {
|
||||
if let contentRules = session.notificationCenter.rules?.global.content as? [MXPushRule] {
|
||||
self.contentRules = contentRules
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ enum DeviceType {
|
||||
case .mobile:
|
||||
return Image(Asset.Images.deviceTypeMobile.name)
|
||||
case .unknown:
|
||||
return Image(Asset.Images.deviceTypeUnknown.name)
|
||||
return Image(Asset.Images.deviceTypeMobile.name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ struct UserSessionCardView: View {
|
||||
} label: {
|
||||
Text(VectorL10n.userSessionVerifyAction)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.padding(.top, 4)
|
||||
@@ -109,7 +110,7 @@ struct UserSessionCardView: View {
|
||||
if viewData.isCurrentSessionDisplayMode, displayMode == .compact {
|
||||
Text(VectorL10n.userSessionViewDetails)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.accent)
|
||||
.foregroundColor(Color(ThemeService.shared().theme.tintColor))
|
||||
.accessibilityIdentifier("userSessionCardViewDetails")
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@@ -481,7 +481,7 @@ private extension UserSessionInfo {
|
||||
case .permanentlyUnverified:
|
||||
return VectorL10n.userSessionPermanentlyUnverifiedSessionDescription
|
||||
case .verified:
|
||||
return VectorL10n.userSessionVerifiedSessionDescription
|
||||
return BWIL10n.userSessionVerifiedSessionDescription
|
||||
case .unknown:
|
||||
return ""
|
||||
}
|
||||
@@ -507,7 +507,7 @@ private extension UserOtherSessionsFilter {
|
||||
case .unverified:
|
||||
return VectorL10n.userSessionUnverifiedSessionDescription
|
||||
case .verified:
|
||||
return VectorL10n.userSessionVerifiedSessionDescription
|
||||
return BWIL10n.userSessionVerifiedSessionDescription
|
||||
case .inactive:
|
||||
return VectorL10n.userSessionInactiveSessionDescription
|
||||
case .all:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user