diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml index 4ecc824424..cf409bcec5 100644 --- a/.github/workflows/triage-incoming.yml +++ b/.github/workflows/triage-incoming.yml @@ -13,3 +13,25 @@ jobs: project: Issue triage column: Incoming repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} + + add_to_triage: + runs-on: ubuntu-latest + if: > + github.repository == 'vector-im/element-x-ios' + steps: + - uses: octokit/graphql-action@v2.x + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4AMlHr" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/CHANGES.md b/CHANGES.md index 7e1ebc95dc..3dad31015f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,22 @@ +## Changes in 1.10.4 (2023-03-07) + +🙌 Improvements + +- CryptoV2: CryptoSDK phased rollout feature ([#7374](https://github.com/vector-im/element-ios/pull/7374)) +- Analytics: Use SwiftPM for AnalyticsEvents instead of CocoaPods ([#7401](https://github.com/vector-im/element-ios/pull/7401)) +- Upgrade MatrixSDK version ([v0.26.0](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.26.0)). +- Session verification: automatically starts scanning for a QR code if we do not have a QR code to display. ([#3115](https://github.com/vector-im/element-ios/issues/3115)) +- Direct Message: manage encrypted DM in case of invite by email ([#6612](https://github.com/vector-im/element-ios/issues/6612)) + +🐛 Bugfixes + +- fix issue on timeline's bubbles not showing proper content after decrypt ([#7397](https://github.com/vector-im/element-ios/pull/7397)) +- Fixes bug about centring user in live location sharing ([#7398](https://github.com/vector-im/element-ios/pull/7398)) +- Polls: improve rendering of poll ended events. ([#7402](https://github.com/vector-im/element-ios/pull/7402)) +- Fix an issue where SAS verification would fail between two iOS devices ([#3946](https://github.com/vector-im/element-ios/issues/3946)) +- Pin SwiftOGG to a release rather than main branch to avoid breaking changes in SwiftOGG causing bugs in element-ios. ([#7388](https://github.com/vector-im/element-ios/issues/7388)) + + ## Changes in 1.10.3 (2023-02-21) 🙌 Improvements diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 7430ec454f..81b3b3fa57 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.3 -CURRENT_PROJECT_VERSION = 1.10.3 +MARKETING_VERSION = 1.10.4 +CURRENT_PROJECT_VERSION = 1.10.4 diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index 35001b1e48..b00f188318 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -92,14 +92,8 @@ class CommonConfiguration: NSObject, Configurable { sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature - if sdkOptions.isCryptoSDKAvailable { - let isEnabled = RiotSettings.shared.enableCryptoSDK - MXLog.debug("[CommonConfiguration] Crypto SDK is \(isEnabled ? "enabled" : "disabled")") - sdkOptions.enableCryptoSDK = isEnabled - sdkOptions.enableStartupProgress = isEnabled - } else { - MXLog.debug("[CommonConfiguration] Crypto SDK is not available)") - } + // Configure Crypto SDK feature deciding which crypto module to use + sdkOptions.cryptoSDKFeature = CryptoSDKFeature.shared } private func makeASCIIUserAgent() -> String? { diff --git a/Config/CryptoSDKConfiguration.swift b/Config/CryptoSDKConfiguration.swift deleted file mode 100644 index 3c922e5473..0000000000 --- a/Config/CryptoSDKConfiguration.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// 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 - -/// Configuration for enabling / disabling Matrix Crypto SDK -@objcMembers class CryptoSDKConfiguration: NSObject { - static let shared = CryptoSDKConfiguration() - - func enable() { - guard MXSDKOptions.sharedInstance().isCryptoSDKAvailable else { - return - } - - RiotSettings.shared.enableCryptoSDK = true - MXSDKOptions.sharedInstance().enableCryptoSDK = true - MXSDKOptions.sharedInstance().enableStartupProgress = true - - MXLog.debug("[CryptoSDKConfiguration] enabling Crypto SDK") - } - - func disable() { - RiotSettings.shared.enableCryptoSDK = false - MXSDKOptions.sharedInstance().enableCryptoSDK = false - MXSDKOptions.sharedInstance().enableStartupProgress = false - - MXLog.debug("[CryptoSDKConfiguration] disabling Crypto SDK") - } -} diff --git a/Podfile b/Podfile index 50655ecd51..0e760e8ca2 100644 --- a/Podfile +++ b/Podfile @@ -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.25.2' +$matrixSDKVersion = '= 0.26.0' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } @@ -70,10 +70,8 @@ abstract_target 'RiotPods' do pod 'WeakDictionary', '~> 2.0' # PostHog for analytics - pod 'PostHog', '~> 1.4.4' + pod 'PostHog', '~> 2.0.0' pod 'Sentry', '~> 7.15.0' - pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift', :inhibit_warnings => false - # pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec' pod 'OLMKit' pod 'zxcvbn-ios' diff --git a/Podfile.lock b/Podfile.lock index eb134a7eed..2d58d808ee 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -14,7 +14,6 @@ PODS: - AFNetworking/Serialization (4.0.1) - AFNetworking/UIKit (4.0.1): - AFNetworking/NSURLSession - - AnalyticsEvents (0.1.0) - BlueCryptor (1.0.32) - BlueECC (1.2.5) - BlueRSA (1.0.200) @@ -38,26 +37,26 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.25.2): - - MatrixSDK/Core (= 0.25.2) - - MatrixSDK/Core (0.25.2): + - MatrixSDK (0.26.0): + - MatrixSDK/Core (= 0.26.0) + - MatrixSDK/Core (0.26.0): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - - MatrixSDKCrypto (= 0.2.0) + - MatrixSDKCrypto (= 0.2.1) - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.25.2): + - MatrixSDK/JingleCallStack (0.26.0): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - - MatrixSDKCrypto (0.2.0) + - MatrixSDKCrypto (0.2.1) - OLMKit (3.2.12): - OLMKit/olmc (= 3.2.12) - OLMKit/olmcpp (= 3.2.12) - OLMKit/olmc (3.2.12) - OLMKit/olmcpp (3.2.12) - - PostHog (1.4.4) + - PostHog (2.0.0) - ReadMoreTextView (3.0.1) - Realm (10.27.0): - Realm/Headers (= 10.27.0) @@ -91,7 +90,6 @@ PODS: - ZXingObjC/All (3.6.5) DEPENDENCIES: - - AnalyticsEvents (from `https://github.com/matrix-org/matrix-analytics-events.git`, branch `release/swift`) - Down (~> 0.11.0) - DSBottomSheet (~> 0.3) - DSWaveformImage (~> 6.1.1) @@ -102,10 +100,10 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.25.2) - - MatrixSDK/JingleCallStack (= 0.25.2) + - MatrixSDK (= 0.26.0) + - MatrixSDK/JingleCallStack (= 0.26.0) - OLMKit - - PostHog (~> 1.4.4) + - PostHog (~> 2.0.0) - ReadMoreTextView (~> 3.0.1) - Reusable (~> 4.1) - Sentry (~> 7.15.0) @@ -164,19 +162,8 @@ SPEC REPOS: - zxcvbn-ios - ZXingObjC -EXTERNAL SOURCES: - AnalyticsEvents: - :branch: release/swift - :git: https://github.com/matrix-org/matrix-analytics-events.git - -CHECKOUT OPTIONS: - AnalyticsEvents: - :commit: 53ad46ba1ea1ee8f21139dda3c351890846a202f - :git: https://github.com/matrix-org/matrix-analytics-events.git - SPEC CHECKSUMS: AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58 - AnalyticsEvents: 0cc8cf52da2fd464a2f39b788a295988151116ce BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 @@ -196,10 +183,10 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 354274127d163af37bdc55093ab96deea1be6a40 - MatrixSDKCrypto: e1ef22aae76b5a6f030ace21a47be83864f4ff44 + MatrixSDK: e324e51eeee6420cdde064d72d8e28e6452f3fb7 + MatrixSDKCrypto: 477d818bf2cc37b6cf702a290eb647bc8cf3cb1b OLMKit: da115f16582e47626616874e20f7bb92222c7a51 - PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f + PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: 9ca328bd7e700cc19703799785e37f77d1a130f2 Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 @@ -217,6 +204,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 20544e99d9acfdfbc4bf98b21d20c1496b9d6fe9 +PODFILE CHECKSUM: 829420858b6f935bbfe58eb25a0533d799ccd78a COCOAPODS: 1.11.3 diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index b721d23003..44f6bd53ca 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -36,6 +36,15 @@ "version" : "5.12.2" } }, + { + "identity" : "matrix-analytics-events", + "kind" : "remoteSourceControl", + "location" : "https://github.com/matrix-org/matrix-analytics-events", + "state" : { + "revision" : "2f5fa5f1e2f6c6ae1a47c33d953a3ce289167eb0", + "version" : "0.5.0" + } + }, { "identity" : "matrix-wysiwyg-composer-swift", "kind" : "remoteSourceControl", @@ -77,7 +86,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vector-im/swift-ogg", "state" : { - "branch" : "main", + "branch" : "0.0.1", "revision" : "e9a9e7601da662fd8b97d93781ff5c60b4becf88" } } diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 7494bbfb40..4cfc866aa1 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -365,6 +365,7 @@ "room_creation_invite_another_user" = "User ID, name or email"; "room_creation_error_invite_user_by_email_without_identity_server" = "No identity server is configured so you cannot add a participant with an email."; "room_creation_dm_error" = "We couldn't create your DM. Please check the users you want to invite and try again."; +"room_creation_only_one_email_invite" = "You can only invite one email at a time"; // Room recents "room_recents_directory_section" = "ROOM DIRECTORY"; @@ -1570,9 +1571,11 @@ Tap the + to start adding people."; "device_verification_self_verify_wait_new_sign_in_title" = "Verify this login"; "device_verification_self_verify_wait_information" = "Verify this session from one of your other sessions, granting it access to encrypted messages.\n\nUse the latest %@ on your other devices:"; "device_verification_self_verify_wait_additional_information" = "This works with %@ and other cross-signing capable Matrix clients."; +"device_verification_self_verify_open_on_other_device_title" = "Open %@ on your other device"; +"device_verification_self_verify_open_on_other_device_information" = "You need to verify this session in order to read your secure message history.\n\nOpen Element on one of your other devices and follow the instructions."; "device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Use Security Key"; "device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Use Security Phrase or Key"; -"device_verification_self_verify_wait_recover_secrets_additional_information" = "If you can't access an existing session"; +"device_verification_self_verify_wait_recover_secrets_additional_help" = "Can't access an existing %@ session?"; "device_verification_self_verify_wait_recover_secrets_checking_availability" = "Checking for other verification capabilities ..."; // MARK: Verify @@ -1748,6 +1751,12 @@ Tap the + to start adding people."; "key_verification_verify_qr_code_scan_other_code_success_title" = "Code validated!"; "key_verification_verify_qr_code_scan_other_code_success_message" = "QR code has been successfully validated."; +"key_verification_scan_qr_code_title" = "Scan QR code"; +"key_verification_scan_qr_code_information_other_user" = "Point your camera at the QR code displayed on their device to verify their session"; +"key_verification_scan_qr_code_information_other_device" = "Point your camera at the QR code displayed on your other device to verify this session"; +"key_verification_scan_qr_code_information_other_session" = "Point your camera at the QR code displayed on your other device to verify your session"; +"key_verification_scan_qr_code_information_new_session" = "Point your camera at the QR code displayed on your other device to verify your new session"; + // MARK: Scan confirmation // Scanning @@ -2287,6 +2296,9 @@ Tap the + to start adding people."; "room_invites_empty_view_title" = "Nothing new."; "room_invites_empty_view_information" = "This is where your invites appear."; +"room_waiting_other_participants_title" = "Waiting for users to join %@"; +"room_waiting_other_participants_message" = "Once invited users have joined %@, you will be able to chat and the room will be end-to-end encrypted"; + // MARK: - Space Selector "space_selector_title" = "My spaces"; diff --git a/Riot/Categories/MXRoomSummary+Riot.m b/Riot/Categories/MXRoomSummary+Riot.m index 3ad3bef65b..c6a55a230e 100644 --- a/Riot/Categories/MXRoomSummary+Riot.m +++ b/Riot/Categories/MXRoomSummary+Riot.m @@ -27,7 +27,7 @@ - (void)setRoomAvatarImageIn:(MXKImageView*)mxkImageView { [mxkImageView vc_setRoomAvatarImageWith:self.avatar roomId:self.roomId - displayName:self.displayname + displayName:self.displayName mediaManager:self.mxSession.mediaManager]; } diff --git a/Riot/Experiments/CryptoSDKFeature.swift b/Riot/Experiments/CryptoSDKFeature.swift new file mode 100644 index 0000000000..fd73ce975e --- /dev/null +++ b/Riot/Experiments/CryptoSDKFeature.swift @@ -0,0 +1,97 @@ +// +// 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 + +/// An implementation of `MXCryptoV2Feature` which uses `UserDefaults` to persist the enabled status +/// of `CryptoSDK`, and which uses feature flags to control rollout availability. +/// +/// The implementation uses both remote and local feature flags to control the availability of `CryptoSDK`. +/// Whilst remote is more convenient in that it allows changes to the rollout without new app releases, +/// it is not available to all users because it requires data tracking user consent. Remote therefore +/// represents the safer, albeit limited rollout strategy, whereas the local feature flags allows eventually +/// targetting all users, but each target change requires new app release. +/// +/// Additionally users can manually enable this feature from the settings if they are not already in the +/// feature group. +@objc class CryptoSDKFeature: NSObject, MXCryptoV2Feature { + @objc static let shared = CryptoSDKFeature() + + var isEnabled: Bool { + RiotSettings.shared.enableCryptoSDK + } + + private static let FeatureName = "ios-crypto-sdk" + private let remoteFeature: RemoteFeaturesClientProtocol + private let localFeature: PhasedRolloutFeature + + init(remoteFeature: RemoteFeaturesClientProtocol = PostHogAnalyticsClient.shared) { + 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 + ) + } + + func enable() { + RiotSettings.shared.enableCryptoSDK = true + Analytics.shared.trackCryptoSDKEnabled() + + MXLog.debug("[CryptoSDKFeature] Crypto SDK enabled") + } + + func enableIfAvailable(forUserId userId: String!) { + guard !isEnabled else { + MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is already enabled") + return + } + + guard let userId else { + MXLog.failure("[CryptoSDKFeature] enableIfAvailable: Missing user id") + return + } + + guard isFeatureEnabled(userId: userId) else { + MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature is currently not available for this user") + return + } + + MXLog.debug("[CryptoSDKFeature] enableIfAvailable: Feature has become available for this user and will be enabled") + enable() + } + + @objc func canManuallyEnable(forUserId userId: String!) -> Bool { + guard let userId else { + MXLog.failure("[CryptoSDKFeature] canManuallyEnable: Missing user id") + return false + } + + // User can manually enable only if not already within the automatic feature group + return !isFeatureEnabled(userId: userId) + } + + @objc func reset() { + RiotSettings.shared.enableCryptoSDK = false + MXLog.debug("[CryptoSDKFeature] Crypto SDK disabled") + } + + private func isFeatureEnabled(userId: String) -> Bool { + remoteFeature.isFeatureEnabled(Self.FeatureName) || localFeature.isEnabled(userId: userId) + } +} diff --git a/Riot/Experiments/Experiment.swift b/Riot/Experiments/Experiment.swift new file mode 100644 index 0000000000..2a1531afc7 --- /dev/null +++ b/Riot/Experiments/Experiment.swift @@ -0,0 +1,50 @@ +// +// 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 +import CryptoKit + +/// Object encapsulating an experiment with an arbitrary number of variants, and a method to deterministically +/// and uniformly assign a variant to user. Variants do not carry any implicit semantics, they are plain numbers +/// to be interpreted by the caller of the experiment. +struct Experiment { + let name: String + let variants: UInt + + /// Get the assigned variant from the total number of variants and for a given `userId` + /// + /// This variant is chosen deterministically (the same `userId` and experiment `name` will yield the same variant) + /// and uniformly (multiple users are distributed roughly evenly among the variants). + func variant(userId: String) -> UInt { + // Combine user id with experiment name to avoid identical variant + // for the same user in different experiments + let data = (userId + name).data(using: .utf8) ?? Data() + + // Get the first 8 bytes and map to decimal number (UInt64 = 8 bytes) + let decimal = digest(for: data) + .prefix(8) + .reduce(0) { $0 << 8 | UInt64($1) } + + // Compress the decimal into a set number of variants using modulo + return UInt(decimal % UInt64(variants)) + } + + private func digest(for data: Data) -> SHA256.Digest { + var sha = SHA256() + sha.update(data: data) + return sha.finalize() + } +} diff --git a/Riot/Experiments/PhasedRolloutFeature.swift b/Riot/Experiments/PhasedRolloutFeature.swift new file mode 100644 index 0000000000..67025fd3e6 --- /dev/null +++ b/Riot/Experiments/PhasedRolloutFeature.swift @@ -0,0 +1,46 @@ +// +// 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 + +/// Object enabling a phased rollout of features depending on the `userId` and `targetPercentage`. +/// +/// The feature uses an experiment under the hood with 100 variants representing 100%. +/// Each userId is deterministically and uniformly assigned a variant, and depending +/// on whether this falls below or above the `targetPercentage` threshold, the feature +/// is considered enabled or disabled. +struct PhasedRolloutFeature { + private let experiment: Experiment + private let targetPercentage: Double + + init(name: String, targetPercentage: Double) { + self.experiment = .init( + name: name, + // 100 variants where each variant represents a single percentage + variants: 100 + ) + self.targetPercentage = targetPercentage + } + + func isEnabled(userId: String) -> Bool { + // Get a bucket number between 0-99 + let variant = experiment.variant(userId: userId) + // Convert to a percentage + let percentage = Double(variant) / 100 + // Consider enabled if falls below rollout target + return percentage < targetPercentage + } +} diff --git a/Riot/Experiments/RemoteFeaturesClientProtocol.swift b/Riot/Experiments/RemoteFeaturesClientProtocol.swift new file mode 100644 index 0000000000..95ca8393e5 --- /dev/null +++ b/Riot/Experiments/RemoteFeaturesClientProtocol.swift @@ -0,0 +1,22 @@ +// +// 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 + +/// A protocol representing a remote features client +protocol RemoteFeaturesClientProtocol { + func isFeatureEnabled(_ feature: String) -> Bool +} diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 96f5ba8fb8..5f533e974a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1871,6 +1871,14 @@ public class VectorL10n: NSObject { public static var deviceVerificationSelfVerifyAlertValidateAction: String { return VectorL10n.tr("Vector", "device_verification_self_verify_alert_validate_action") } + /// You need to verify this session in order to read your secure message history.\n\nOpen Element on one of your other devices and follow the instructions. + public static var deviceVerificationSelfVerifyOpenOnOtherDeviceInformation: String { + return VectorL10n.tr("Vector", "device_verification_self_verify_open_on_other_device_information") + } + /// Open %@ on your other device + public static func deviceVerificationSelfVerifyOpenOnOtherDeviceTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "device_verification_self_verify_open_on_other_device_title", p1) + } /// Use this session to verify your new one, granting it access to encrypted messages. public static var deviceVerificationSelfVerifyStartInformation: String { return VectorL10n.tr("Vector", "device_verification_self_verify_start_information") @@ -1895,9 +1903,9 @@ public class VectorL10n: NSObject { public static var deviceVerificationSelfVerifyWaitNewSignInTitle: String { return VectorL10n.tr("Vector", "device_verification_self_verify_wait_new_sign_in_title") } - /// If you can't access an existing session - public static var deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalInformation: String { - return VectorL10n.tr("Vector", "device_verification_self_verify_wait_recover_secrets_additional_information") + /// Can't access an existing %@ session? + public static func deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalHelp(_ p1: String) -> String { + return VectorL10n.tr("Vector", "device_verification_self_verify_wait_recover_secrets_additional_help", p1) } /// Checking for other verification capabilities ... public static var deviceVerificationSelfVerifyWaitRecoverSecretsCheckingAvailability: String { @@ -2987,6 +2995,26 @@ public class VectorL10n: NSObject { public static func keyVerificationScanConfirmationScanningUserWaitingOther(_ p1: String) -> String { return VectorL10n.tr("Vector", "key_verification_scan_confirmation_scanning_user_waiting_other", p1) } + /// Point your camera at the QR code displayed on your other device to verify your new session + public static var keyVerificationScanQrCodeInformationNewSession: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_new_session") + } + /// Point your camera at the QR code displayed on your other device to verify this session + public static var keyVerificationScanQrCodeInformationOtherDevice: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_other_device") + } + /// Point your camera at the QR code displayed on your other device to verify your session + public static var keyVerificationScanQrCodeInformationOtherSession: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_other_session") + } + /// Point your camera at the QR code displayed on their device to verify their session + public static var keyVerificationScanQrCodeInformationOtherUser: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_information_other_user") + } + /// Scan QR code + public static var keyVerificationScanQrCodeTitle: String { + return VectorL10n.tr("Vector", "key_verification_scan_qr_code_title") + } /// Other users may not trust it. public static var keyVerificationSelfVerifyCurrentSessionAlertMessage: String { return VectorL10n.tr("Vector", "key_verification_self_verify_current_session_alert_message") @@ -5239,6 +5267,10 @@ public class VectorL10n: NSObject { public static var roomCreationNameTitle: String { return VectorL10n.tr("Vector", "room_creation_name_title") } + /// You can only invite one email at a time + public static var roomCreationOnlyOneEmailInvite: String { + return VectorL10n.tr("Vector", "room_creation_only_one_email_invite") + } /// (e.g. @bob:homeserver1; @john:homeserver2...) public static var roomCreationParticipantsPlaceholder: String { return VectorL10n.tr("Vector", "room_creation_participants_placeholder") @@ -6595,6 +6627,14 @@ public class VectorL10n: NSObject { public static var roomUnsentMessagesUnknownDevicesNotification: String { return VectorL10n.tr("Vector", "room_unsent_messages_unknown_devices_notification") } + /// Once invited users have joined %@, you will be able to chat and the room will be end-to-end encrypted + public static func roomWaitingOtherParticipantsMessage(_ p1: String) -> String { + return VectorL10n.tr("Vector", "room_waiting_other_participants_message", p1) + } + /// Waiting for users to join %@ + public static func roomWaitingOtherParticipantsTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "room_waiting_other_participants_title", p1) + } /// End-to-end encryption is in beta and may not be reliable.\n\nYou should not yet trust it to secure data.\n\nDevices will not yet be able to decrypt history from before they joined the room.\n\nEncrypted messages will not be visible on clients that do not yet implement encryption. public static var roomWarningAboutEncryption: String { return VectorL10n.tr("Vector", "room_warning_about_encryption") diff --git a/Riot/Managers/Call/CallPresenter.swift b/Riot/Managers/Call/CallPresenter.swift index 9d9465f8b6..721f13079a 100644 --- a/Riot/Managers/Call/CallPresenter.swift +++ b/Riot/Managers/Call/CallPresenter.swift @@ -208,7 +208,7 @@ class CallPresenter: NSObject { if error == nil { JMCallKitProxy.reportCallUpdate(with: newUUID, handle: roomId, - displayName: room.summary.displayname, + displayName: room.summary.displayName, hasVideo: true) JMCallKitProxy.reportOutgoingCall(with: newUUID, connectedAt: nil) diff --git a/Riot/Model/Room/RoomPreviewData.m b/Riot/Model/Room/RoomPreviewData.m index f619344ed2..af751ab044 100644 --- a/Riot/Model/Room/RoomPreviewData.m +++ b/Riot/Model/Room/RoomPreviewData.m @@ -123,7 +123,7 @@ - (void)peekInRoom:(void (^)(BOOL succeeded))completion [self.roomDataSource finalizeInitialization]; self.roomDataSource.markTimelineInitialEvent = YES; - self->_roomName = peekingRoom.summary.displayname; + self->_roomName = peekingRoom.summary.displayName; self->_roomAvatarUrl = peekingRoom.summary.avatar; self->_roomTopic = [MXTools stripNewlineCharacters:peekingRoom.summary.topic];; diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index 60e1560b9b..c48b447e58 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -24,17 +24,8 @@ import AnalyticsEvents /// non-fatal issues and performance. `Analytics` class serves as a façade /// to all these use cases. /// -/// ## Creating Analytics Events -/// -/// Events are managed in a shared repo for all Element clients https://github.com/matrix-org/matrix-analytics-events -/// To add a new event create a PR to that repo with the new/updated schema. Element's Podfile has -/// a local version of the pod (commented out) for development purposes. -/// Once merged into `main`, follow the steps below to integrate the changes into the project: -/// 1. Check if `main` contains any source breaking changes to the events. If so, please -/// wait until you are ready to merge your work into element-ios. -/// 2. Merge `main` into the `release/swift` branch. -/// 3. Run `bundle exec pod update AnalyticsEvents` to update the pod. -/// 4. Make sure to commit `Podfile.lock` with the new commit hash. +/// Events are managed in a shared repo for all Element clients +/// https://github.com/matrix-org/matrix-analytics-events and integrated via SwiftPM /// @objcMembers class Analytics: NSObject { @@ -44,7 +35,7 @@ import AnalyticsEvents static let shared = Analytics() /// The analytics client to send events with. - private var client: AnalyticsClientProtocol = PostHogAnalyticsClient() + private var client: AnalyticsClientProtocol = PostHogAnalyticsClient.shared /// The monitoring client to track crashes, issues and performance private var monitoringClient = SentryMonitoringClient() @@ -230,10 +221,10 @@ extension Analytics { /// /// Only non-nil properties will be updated when calling this method. func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil, numFavouriteRooms: Int? = nil, numSpaces: Int? = nil, allChatsActiveFilter: UserSessionProperties.AllChatsActiveFilter? = nil) { - let userProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: ftueUseCase?.analyticsName, + let userProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: allChatsActiveFilter?.analyticsName, + ftueUseCaseSelection: ftueUseCase?.analyticsName, numFavouriteRooms: numFavouriteRooms, - numSpaces: numSpaces, - allChatsActiveFilter: allChatsActiveFilter?.analyticsName) + numSpaces: numSpaces) client.updateUserProperties(userProperties) } @@ -281,7 +272,12 @@ extension Analytics { /// - reason: The error that occurred. /// - context: Additional context of the error that occured func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { - let event = AnalyticsEvent.Error(context: context, domain: .E2EE, name: reason.errorName) + let event = AnalyticsEvent.Error( + context: context, + cryptoModule: MXSDKOptions.sharedInstance().enableCryptoSDK ? .Rust : .Native, + domain: .E2EE, + name: reason.errorName + ) capture(event: event) } @@ -359,7 +355,7 @@ extension Analytics: MXAnalyticsDelegate { func trackCallError(with reason: __MXCallHangupReason, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) { let callEvent = AnalyticsEvent.CallError(isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming) - let event = AnalyticsEvent.Error(context: nil, domain: .VOIP, name: reason.errorName) + let event = AnalyticsEvent.Error(context: nil, cryptoModule: nil, domain: .VOIP, name: reason.errorName) capture(event: callEvent) capture(event: event) } diff --git a/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift b/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift deleted file mode 100644 index e0d2192797..0000000000 --- a/Riot/Modules/Analytics/Helpers/UserProperties+Element.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright 2021 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 -import AnalyticsEvents - -extension AnalyticsEvent.UserProperties { - - // Initializer for Element. Strips all Web properties. - public init(ftueUseCaseSelection: FtueUseCaseSelection?, numFavouriteRooms: Int?, numSpaces: Int?, allChatsActiveFilter: AllChatsActiveFilter?) { - self.init(WebMetaSpaceFavouritesEnabled: nil, - WebMetaSpaceHomeAllRooms: nil, - WebMetaSpaceHomeEnabled: nil, - WebMetaSpaceOrphansEnabled: nil, - WebMetaSpacePeopleEnabled: nil, - allChatsActiveFilter: allChatsActiveFilter, - ftueUseCaseSelection: ftueUseCaseSelection, - numFavouriteRooms: numFavouriteRooms, - numSpaces: numSpaces) - } - -} diff --git a/Riot/Modules/Analytics/PostHogAnalyticsClient.swift b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift index 6b27affea8..ec49716bed 100644 --- a/Riot/Modules/Analytics/PostHogAnalyticsClient.swift +++ b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift @@ -25,6 +25,8 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { /// Any user properties to be included with the next captured event. private(set) var pendingUserProperties: AnalyticsEvent.UserProperties? + static let shared = PostHogAnalyticsClient() + var isRunning: Bool { postHog?.enabled ?? false } func start() { @@ -79,10 +81,10 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { } // Merge the updated user properties with the existing ones - self.pendingUserProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection, + self.pendingUserProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: userProperties.allChatsActiveFilter ?? pendingUserProperties.allChatsActiveFilter, + ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection, numFavouriteRooms: userProperties.numFavouriteRooms ?? pendingUserProperties.numFavouriteRooms, - numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces, - allChatsActiveFilter: userProperties.allChatsActiveFilter ?? pendingUserProperties.allChatsActiveFilter) + numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces) } // MARK: - Private @@ -102,3 +104,9 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { return properties } } + +extension PostHogAnalyticsClient: RemoteFeaturesClientProtocol { + func isFeatureEnabled(_ feature: String) -> Bool { + postHog?.isFeatureEnabled(feature) == true + } +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 5944cccdb6..208cb46ebb 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2184,7 +2184,7 @@ - (void)logoutSendingRequestServer:(BOOL)sendLogoutServerRequest [self clearCache]; // Reset Crypto SDK configuration (labs flag for which crypto module to use) - [CryptoSDKConfiguration.shared disable]; + [CryptoSDKFeature.shared reset]; // Reset key backup banner preferences [SecureBackupBannerPreferences.shared reset]; diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m index a8019c06c2..3e8227e7cf 100644 --- a/Riot/Modules/Call/CallViewController.m +++ b/Riot/Modules/Call/CallViewController.m @@ -484,7 +484,7 @@ - (UIImage*)picturePlaceholder else if (self.mxCall.room) { return [AvatarGenerator generateAvatarForMatrixItem:self.mxCall.room.roomId - withDisplayName:self.mxCall.room.summary.displayname + withDisplayName:self.mxCall.room.summary.displayName size:self.callerImageViewWidthConstraint.constant andFontSize:fontSize]; } diff --git a/Riot/Modules/Call/PiP/CallPiPView.swift b/Riot/Modules/Call/PiP/CallPiPView.swift index 4a553a3d0f..97f06caf66 100644 --- a/Riot/Modules/Call/PiP/CallPiPView.swift +++ b/Riot/Modules/Call/PiP/CallPiPView.swift @@ -158,7 +158,7 @@ class CallPiPView: UIView { andFontSize: fontSize) } else if let room = call?.room { return AvatarGenerator.generateAvatar(forMatrixItem: room.roomId, - withDisplayName: room.summary.displayname, + withDisplayName: room.summary.displayName, size: imageView.bounds.width, andFontSize: fontSize) } diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 955a07f23c..85393a2d0d 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -1103,7 +1103,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N RecentEmptySectionTableViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:[RecentEmptySpaceSectionTableViewCell defaultReuseIdentifier]]; tableViewCell.iconView.image = [ThemeService.shared isCurrentThemeDark] ? AssetImages.allChatsEmptySpaceArtworkDark.image : AssetImages.allChatsEmptySpaceArtwork.image; - tableViewCell.titleLabel.text = [VectorL10n allChatsEmptyViewTitle: self.currentSpace.summary.displayname]; + tableViewCell.titleLabel.text = [VectorL10n allChatsEmptyViewTitle: self.currentSpace.summary.displayName]; tableViewCell.messageLabel.text = VectorL10n.allChatsEmptySpaceInformation; return tableViewCell; @@ -1670,7 +1670,7 @@ - (void)moveRoomCell:(MXRoom*)room from:(NSIndexPath*)oldPath to:(NSIndexPath*)n NSString* tagOrder = [room.mxSession tagOrderToBeAtIndex:newPath.row from:oldPos withTag:dstRoomTag]; - MXLogDebug(@"[RecentsDataSource] Update the room %@ [%@] tag from %@ to %@ with tag order %@", room.roomId, room.summary.displayname, oldRoomTag, dstRoomTag, tagOrder); + MXLogDebug(@"[RecentsDataSource] Update the room %@ [%@] tag from %@ to %@ with tag order %@", room.roomId, room.summary.displayName, oldRoomTag, dstRoomTag, tagOrder); [room replaceTag:oldRoomTag byTag:dstRoomTag diff --git a/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift index bc95f1f94a..1bfdd60d47 100644 --- a/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift +++ b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift @@ -95,7 +95,7 @@ public class MockRecentsListService: NSObject, RecentsListServiceProtocol { } else if i % 11 == 0 { room.dataTypes = .serverNotice } - room.displayname = "Room \(i+1)" + room.displayName = "Room \(i+1)" if let event = MXEvent(fromJSON: [ "event_id": MXTools.generateTransactionId() as Any, "room_id": room.roomId, diff --git a/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift b/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift index 3503282ab5..88596c75fe 100644 --- a/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift +++ b/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift @@ -26,7 +26,7 @@ public class MockRoomSummary: NSObject, MXRoomSummaryProtocol { public var avatar: String? - public var displayname: String? + public var displayName: String? public var topic: String? diff --git a/Riot/Modules/Contacts/Details/Views/RoomTableViewCell.m b/Riot/Modules/Contacts/Details/Views/RoomTableViewCell.m index 22036093c5..ce6ee4399d 100644 --- a/Riot/Modules/Contacts/Details/Views/RoomTableViewCell.m +++ b/Riot/Modules/Contacts/Details/Views/RoomTableViewCell.m @@ -48,7 +48,7 @@ - (void)render:(MXRoom *)room { [room.summary setRoomAvatarImageIn:self.avatarImageView]; - self.titleLabel.text = room.summary.displayname; + self.titleLabel.text = room.summary.displayName; } + (CGFloat)cellHeight diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift index c63b6318fc..96abf93615 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsEditActionProvider.swift @@ -40,7 +40,7 @@ class AllChatsEditActionProvider { private var rootSpaceCount: Int = 0 private var parentSpace: MXSpace? { didSet { - parentName = parentSpace?.summary?.displayname ?? VectorL10n.spaceTag + parentName = parentSpace?.summary?.displayName ?? VectorL10n.spaceTag } } private var parentName: String = VectorL10n.spaceTag diff --git a/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift index 0d9e3d2fa4..1d13e26de6 100644 --- a/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift +++ b/Riot/Modules/ContextMenu/ActionProviders/AllChatsSpaceActionProvider.swift @@ -39,7 +39,7 @@ class AllChatsSpaceActionProvider { private var currentSpace: MXSpace? { didSet { - spaceName = currentSpace?.summary?.displayname ?? VectorL10n.spaceTag + spaceName = currentSpace?.summary?.displayName ?? VectorL10n.spaceTag } } private var spaceName: String = VectorL10n.spaceTag diff --git a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m index b77185d106..de5dafaeff 100644 --- a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m +++ b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m @@ -100,7 +100,7 @@ - (void)setShouldShowRoomDisplayName:(BOOL)shouldShowRoomDisplayName2 MXRoom *room = [searchDataSource.mxSession roomWithRoomId:roomId]; if (room) { - roomDisplayName = room.summary.displayname; + roomDisplayName = room.summary.displayName; if (!roomDisplayName.length) { roomDisplayName = [VectorL10n roomDisplaynameEmptyRoom]; diff --git a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m index 32e8edcd95..0e5a0312b9 100644 --- a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m +++ b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m @@ -41,7 +41,7 @@ - (void)render:(MXKCellData *)cellData MXRoom* room = [bubbleData.mxSession roomWithRoomId:bubbleData.roomId]; if (room) { - self.roomNameLabel.text = room.summary.displayname; + self.roomNameLabel.text = room.summary.displayName; if (!self.roomNameLabel.text.length) { self.roomNameLabel.text = [VectorL10n roomDisplaynameEmptyRoom]; diff --git a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m index 780d11efe2..6b68de2fa1 100644 --- a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m +++ b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m @@ -41,7 +41,7 @@ - (void)render:(MXKCellData *)cellData MXRoom* room = [bubbleData.mxSession roomWithRoomId:bubbleData.roomId]; if (room) { - self.roomNameLabel.text = room.summary.displayname; + self.roomNameLabel.text = room.summary.displayName; } else { diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index 2ec3bbe8b0..1c72df3425 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -415,7 +415,7 @@ class AllChatsViewController: HomeViewController { let title: String let informationText: String if let currentSpace = self.dataSource?.currentSpace { - title = VectorL10n.allChatsEmptyViewTitle(currentSpace.summary?.displayname ?? VectorL10n.spaceTag) + title = VectorL10n.allChatsEmptyViewTitle(currentSpace.summary?.displayName ?? VectorL10n.spaceTag) informationText = VectorL10n.allChatsEmptySpaceInformation } else { let myUser = mainSession.myUser @@ -495,7 +495,7 @@ class AllChatsViewController: HomeViewController { private func updateUI() { let currentSpace = self.dataSource?.currentSpace - self.title = currentSpace?.summary?.displayname ?? VectorL10n.allChatsTitle + self.title = currentSpace?.summary?.displayName ?? VectorL10n.allChatsTitle setupEditOptions() updateToolbar(with: editActionProvider.updateMenu(with: mainSession, parentSpace: currentSpace, completion: { [weak self] menu in @@ -638,7 +638,7 @@ class AllChatsViewController: HomeViewController { return } - let name = spaceSummary.displayname ?? VectorL10n.spaceTag + let name = spaceSummary.displayName ?? VectorL10n.spaceTag let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle, selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms, diff --git a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m index c8fa03fdd1..39552e4f0a 100644 --- a/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m +++ b/Riot/Modules/Integrations/Widgets/Jitsi/JitsiViewController.m @@ -272,7 +272,7 @@ - (void)joinConferenceWithId:(NSString*)conferenceId andServerUrl:(NSURL*)server builder.room = conferenceId; builder.videoMuted = !self.startWithVideo; - builder.subject = roomSummary.displayname; + builder.subject = roomSummary.displayName; builder.userInfo = [[JitsiMeetUserInfo alloc] initWithDisplayName:userDisplayName andEmail:nil andAvatar:avatarUrl]; diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift index 4129c4f478..71220203d2 100644 --- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift +++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift @@ -252,6 +252,9 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType { } private func showVerifyBySAS(transaction: MXSASTransaction, animated: Bool) { + if navigationRouter.modules.last is KeyVerificationVerifyBySASCoordinator { + return + } let coordinator = KeyVerificationVerifyBySASCoordinator(session: self.session, transaction: transaction, verificationKind: self.verificationKind) coordinator.delegate = self coordinator.start() diff --git a/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard b/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard index b6b9edafa2..f353aaa832 100644 --- a/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard +++ b/Riot/Modules/KeyVerification/Common/ScanConfirmation/KeyVerificationScanConfirmationViewController.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -13,38 +11,38 @@ - + - + - + - + - + - + @@ -178,6 +178,7 @@ Use the latest Riot on your other devices: + @@ -185,10 +186,8 @@ Use the latest Riot on your other devices: - - @@ -198,6 +197,7 @@ Use the latest Riot on your other devices: + diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift index f7969bf22f..91d3920ba5 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift @@ -30,13 +30,12 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { // MARK: Outlets + @IBOutlet private weak var titleLabel: UILabel! @IBOutlet private weak var informationLabel: UILabel! @IBOutlet private weak var desktopClientImageView: UIImageView! @IBOutlet private weak var mobileClientImageView: UIImageView! - - @IBOutlet private weak var additionalInformationLabel: UILabel! - + @IBOutlet private weak var recoverSecretsAvailabilityLoadingContainerView: UIView! @IBOutlet private weak var recoverSecretsAvailabilityLoadingLabel: UILabel! @IBOutlet private weak var recoverSecretsAvailabilityActivityIndicatorView: UIActivityIndicatorView! @@ -70,7 +69,6 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { super.viewDidLoad() // Do any additional setup after loading the view. - self.setupViews() self.activityPresenter = ActivityIndicatorPresenter() self.errorPresenter = MXKErrorAlertPresentation() @@ -96,11 +94,11 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { if let navigationBar = self.navigationController?.navigationBar { theme.applyStyle(onNavigationBar: navigationBar) } - - self.informationLabel.textColor = theme.textPrimaryColor + + self.titleLabel.textColor = theme.textPrimaryColor + self.informationLabel.textColor = theme.textSecondaryColor self.desktopClientImageView.tintColor = theme.tintColor self.mobileClientImageView.tintColor = theme.tintColor - self.additionalInformationLabel.textColor = theme.textPrimaryColor self.recoverSecretsAvailabilityLoadingLabel.textColor = theme.textSecondaryColor self.recoverSecretsAvailabilityActivityIndicatorView.color = theme.tintColor } @@ -125,16 +123,13 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { self.cancelBarButtonItem = cancelBarButtonItem } - self.title = VectorL10n.deviceVerificationSelfVerifyWaitTitle - - self.informationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitInformation(AppInfo.current.displayName) + self.titleLabel.text = VectorL10n.deviceVerificationSelfVerifyOpenOnOtherDeviceTitle(AppInfo.current.displayName) + self.informationLabel.text = VectorL10n.deviceVerificationSelfVerifyOpenOnOtherDeviceInformation self.desktopClientImageView.image = Asset.Images.monitor.image.withRenderingMode(.alwaysTemplate) self.mobileClientImageView.image = Asset.Images.smartphone.image.withRenderingMode(.alwaysTemplate) - - self.additionalInformationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitAdditionalInformation(AppInfo.current.displayName) - - self.recoverSecretsAdditionalInformationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalInformation + + self.recoverSecretsAdditionalInformationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitRecoverSecretsAdditionalHelp(AppInfo.current.displayName) } private func render(viewState: KeyVerificationSelfVerifyWaitViewState) { @@ -168,7 +163,6 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { private func renderLoaded(viewData: KeyVerificationSelfVerifyWaitViewData) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) - self.title = viewData.isNewSignIn ? VectorL10n.deviceVerificationSelfVerifyWaitNewSignInTitle : VectorL10n.deviceVerificationSelfVerifyWaitTitle self.cancelBarButtonItem?.title = viewData.isNewSignIn ? VectorL10n.skip : VectorL10n.cancel let hideRecoverSecrets: Bool diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift index 5d4830bede..2f1b8a2ce0 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift @@ -285,7 +285,8 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai private func sasTransactionDidStateChange(_ transaction: MXSASTransaction) { switch transaction.state { case MXSASTransactionStateIncomingShowAccept: - transaction.accept() + // The transaction will be automatically accepted by the MXKeyVerificationManager when the SAS start event is handled + break case MXSASTransactionStateShowSAS: self.unregisterTransactionDidStateChangeNotification() self.coordinatorDelegate?.keyVerificationSelfVerifyWaitViewModel(self, didAcceptIncomingSASTransaction: transaction) diff --git a/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m index 4847ffef7e..686215f843 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m @@ -503,7 +503,7 @@ - (void)updatePeerInfoDisplay } else if (mxCall.isConferenceCall) { - peerDisplayName = mxCall.room.summary.displayname; + peerDisplayName = mxCall.room.summary.displayName; peerAvatarURL = mxCall.room.summary.avatar; } diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSourceManager.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSourceManager.m index 181552e536..b0b9bb7adb 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSourceManager.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSourceManager.m @@ -26,10 +26,21 @@ @interface MXKRoomDataSourceManager() */ NSMutableDictionary *roomDataSources; + /** + The list of rooms with a "late decryption" event. Causing bubbles issues + Each element is a room ID. + */ + NSMutableSet *roomDataSourcesToDestroy; + /** Observe UIApplicationDidReceiveMemoryWarningNotification to dispose of any resources that can be recreated. */ id UIApplicationDidReceiveMemoryWarningNotificationObserver; + + /** + Observe kMXEventDidDecryptNotification to get late decrypted events. + */ + id mxEventDidDecryptNotificationObserver; } @end @@ -119,6 +130,7 @@ - (instancetype)initWithMatrixSession:(MXSession *)matrixSession { mxSession = matrixSession; roomDataSources = [NSMutableDictionary dictionary]; + roomDataSourcesToDestroy = [NSMutableSet set]; _releasePolicy = MXKRoomDataSourceManagerReleasePolicyNeverRelease; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionDidLeaveRoom:) name:kMXSessionDidLeaveRoomNotification object:nil]; @@ -138,6 +150,12 @@ - (instancetype)initWithMatrixSession:(MXSession *)matrixSession } }]; + + // Observe late decrypted events, and store rooms ids in memory + mxEventDidDecryptNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXEventDidDecryptNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + MXEvent *decryptedEvent = notif.object; + [self->roomDataSourcesToDestroy addObject:decryptedEvent.roomId]; + }]; } return self; } @@ -156,6 +174,11 @@ - (void)destroy [[NSNotificationCenter defaultCenter] removeObserver:UIApplicationDidReceiveMemoryWarningNotificationObserver]; UIApplicationDidReceiveMemoryWarningNotificationObserver = nil; } + if (mxEventDidDecryptNotificationObserver) + { + [[NSNotificationCenter defaultCenter] removeObserver:mxEventDidDecryptNotificationObserver]; + mxEventDidDecryptNotificationObserver = nil; + } } #pragma mark @@ -202,9 +225,19 @@ - (void)roomDataSourceForRoom:(NSString *)roomId create:(BOOL)create onComplete: // If not available yet, create the room data source MXKRoomDataSource *roomDataSource = roomDataSources[roomId]; - + + // check if the room's dataSource has events with late decryption issues and destroys it + BOOL roomDataSourceToBeDestroyed = [roomDataSourcesToDestroy containsObject:roomId]; + + if (roomDataSource && roomDataSourceToBeDestroyed && create) { + [roomDataSource destroy]; + roomDataSources[roomId] = nil; + roomDataSource = nil; + } + if (!roomDataSource && create && roomId) { + [roomDataSourcesToDestroy removeObject:roomId]; [_roomDataSourceClass loadRoomDataSourceWithRoomId:roomId threadId:nil andMatrixSession:mxSession onComplete:^(id roomDataSource) { [self addRoomDataSource:roomDataSource]; onComplete(roomDataSource); diff --git a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m index 474494d59c..f4f0be8331 100644 --- a/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m +++ b/Riot/Modules/MatrixKit/Models/RoomList/MXKRecentCellData.m @@ -86,7 +86,7 @@ - (NSString *)roomDisplayname { return self.roomSummary.spaceChildInfo.displayName; } - return roomSummary.displayname; + return roomSummary.displayName; } - (NSString *)avatarUrl diff --git a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m index d1daa83ad8..a5e76b03a8 100644 --- a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m +++ b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m @@ -45,7 +45,7 @@ - (instancetype)initWithSearchResult:(MXSearchResult *)searchResult2 andSearchDa MXRoom *room = [searchDataSource.mxSession roomWithRoomId:searchResult.result.roomId]; if (room) { - title = room.summary.displayname; + title = room.summary.displayName; } else { diff --git a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleCreationTableViewCell.m b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleCreationTableViewCell.m index c19d4bfe5a..725354b6f6 100644 --- a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleCreationTableViewCell.m +++ b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleCreationTableViewCell.m @@ -169,7 +169,7 @@ - (IBAction)onButtonPressed:(id)sender if ((row >= 0) && (row < rooms.count)) { MXRoom* room = [rooms objectAtIndex:row]; - _inputTextField.text = room.summary.displayname; + _inputTextField.text = room.summary.displayName; _addButton.enabled = YES; } @@ -191,7 +191,7 @@ - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSIn rooms = [_mxSession.rooms sortedArrayUsingComparator:^NSComparisonResult(MXRoom* firstRoom, MXRoom* secondRoom) { // Alphabetic order - return [firstRoom.summary.displayname compare:secondRoom.summary.displayname options:NSCaseInsensitiveSearch]; + return [firstRoom.summary.displayName compare:secondRoom.summary.displayName options:NSCaseInsensitiveSearch]; }]; return rooms.count; @@ -202,7 +202,7 @@ - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSIn - (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { MXRoom* room = [rooms objectAtIndex:row]; - return room.summary.displayname; + return room.summary.displayName; } @end diff --git a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m index 79632d87ce..c081f51f63 100644 --- a/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m +++ b/Riot/Modules/MatrixKit/Views/PushRule/MXKPushRuleTableViewCell.m @@ -71,7 +71,7 @@ - (void)setMxPushRule:(MXPushRule *)mxPushRule MXRoom *room = [_mxSession roomWithRoomId:mxPushRule.ruleId]; if (room) { - description = [VectorL10n notificationSettingsRoomRuleTitle:room.summary.displayname]; + description = [VectorL10n notificationSettingsRoomRuleTitle:room.summary.displayName]; } break; } diff --git a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleView.m b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleView.m index c86fbe0d21..60a3782219 100644 --- a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleView.m +++ b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleView.m @@ -82,7 +82,7 @@ - (void)refreshDisplay if (_mxRoom) { // Replace empty string by nil : avoid having the placeholder 'Room name" when there is no displayname - self.displayNameTextField.text = (_mxRoom.summary.displayname.length) ? _mxRoom.summary.displayname : nil; + self.displayNameTextField.text = (_mxRoom.summary.displayName.length) ? _mxRoom.summary.displayName : nil; } else if (_mxUser) { @@ -189,7 +189,7 @@ - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField if (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomName]) { // Only the room name is edited here, update the text field with the room name - textField.text = _mxRoom.summary.displayname; + textField.text = _mxRoom.summary.displayName; textField.backgroundColor = [UIColor whiteColor]; } else @@ -236,7 +236,7 @@ - (void)textFieldDidEndEditing:(UITextField *)textField textField.backgroundColor = [UIColor clearColor]; NSString *roomName = textField.text; - if ((roomName.length || _mxRoom.summary.displayname.length) && [roomName isEqualToString:_mxRoom.summary.displayname] == NO) + if ((roomName.length || _mxRoom.summary.displayName.length) && [roomName isEqualToString:_mxRoom.summary.displayName] == NO) { if ([self.delegate respondsToSelector:@selector(roomTitleView:isSaving:)]) { @@ -266,7 +266,7 @@ - (void)textFieldDidEndEditing:(UITextField *)textField } // Revert change - textField.text = strongSelf.mxRoom.summary.displayname; + textField.text = strongSelf.mxRoom.summary.displayName; MXLogDebug(@"[MXKRoomTitleView] Rename room failed"); // Notify MatrixKit user NSString *myUserId = strongSelf.mxRoom.mxSession.myUser.userId; @@ -278,7 +278,7 @@ - (void)textFieldDidEndEditing:(UITextField *)textField else { // No change on room name, restore title with room displayName - textField.text = _mxRoom.summary.displayname; + textField.text = _mxRoom.summary.displayName; } } } diff --git a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m index b4dc7d4e12..0099053d64 100644 --- a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m +++ b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m @@ -367,7 +367,7 @@ - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField if (userPowerLevel >= [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kMXEventTypeStringRoomName]) { // Only the room name is edited here, update the text field with the room name - textField.text = self.mxRoom.summary.displayname; + textField.text = self.mxRoom.summary.displayName; textField.backgroundColor = [UIColor whiteColor]; } else diff --git a/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift b/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift new file mode 100644 index 0000000000..ecf9ca5a09 --- /dev/null +++ b/Riot/Modules/QRCode/Reader/QRCodeReaderView.swift @@ -0,0 +1,221 @@ +// +// 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 UIKit +import ZXingObjC +import Combine + +final class QRCodeReaderView: UIView { + + // MARK: Public + + var didFoundData: (Data) -> Void = { _ in } + + // MARK: Private + + private lazy var zxCapture: ZXCapture = ZXCapture() + private var captureSizeTransform: CGAffineTransform? + private var isScanning: Bool = false + private var isFirstApplyOrientation: Bool = false + + private var rotationObserver: AnyCancellable? + + init() { + super.init(frame: .zero) + setup() + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + deinit { +#if !targetEnvironment(simulator) + self.zxCapture.layer.removeFromSuperlayer() + self.zxCapture.hard_stop() +#endif + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if superview == nil { + stopScanning() + } + } + + // MARK: - Public + + func startScanning() { +#if !targetEnvironment(simulator) + self.zxCapture.start() +#endif + isScanning = true + } + + func stopScanning() { +#if !targetEnvironment(simulator) + self.zxCapture.stop() +#endif + isScanning = false + } + + // MARK: - Private + + override func layoutSubviews() { + super.layoutSubviews() + + guard isFirstApplyOrientation == false else { + return + } + + isFirstApplyOrientation = true + applyOrientation() + } + + private func setup() { + isUserInteractionEnabled = true + clipsToBounds = true + self.setupQRCodeReaderView() + + rotationObserver = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification) + .sink(receiveValue: { [weak self] _ in + self?.applyOrientation() + }) + } + + private func setupQRCodeReaderView() { +#if !targetEnvironment(simulator) + zxCapture.delegate = self + zxCapture.camera = zxCapture.back() + zxCapture.layer.frame = self.bounds + self.layer.addSublayer(zxCapture.layer) +#endif + } + + private func applyOrientation() { + + let orientation = UIApplication.shared.statusBarOrientation + let captureRotation: Double + let scanRectRotation: Double + + switch orientation { + case .portrait: + captureRotation = 0 + scanRectRotation = 90 + case .landscapeLeft: + captureRotation = 90 + scanRectRotation = 180 + case .landscapeRight: + captureRotation = 270 + scanRectRotation = 0 + case .portraitUpsideDown: + captureRotation = 180 + scanRectRotation = 270 + default: + captureRotation = 0 + scanRectRotation = 90 + } + + applyRectOfInterest(orientation: orientation) + + let angleRadius = captureRotation / 180.0 * Double.pi + let captureTranform = CGAffineTransform(rotationAngle: CGFloat(angleRadius)) + + zxCapture.transform = captureTranform + zxCapture.rotation = CGFloat(scanRectRotation) + zxCapture.layer.frame = self.bounds + } + + private func applyRectOfInterest(orientation: UIInterfaceOrientation) { + var transformedVideoRect = self.frame + let cameraSessionPreset = zxCapture.sessionPreset + + var scaleVideoX, scaleVideoY: CGFloat + var videoHeight, videoWidth: CGFloat + + // Currently support only for 1920x1080 || 1280x720 + if cameraSessionPreset == AVCaptureSession.Preset.hd1920x1080.rawValue { + videoHeight = 1080.0 + videoWidth = 1920.0 + } else { + videoHeight = 720.0 + videoWidth = 1280.0 + } + + if orientation == UIInterfaceOrientation.portrait { + scaleVideoX = self.frame.width / videoHeight + scaleVideoY = self.frame.height / videoWidth + + // Convert CGPoint under portrait mode to map with orientation of image + // because the image will be cropped before rotate + // reference: https://github.com/TheLevelUp/ZXingObjC/issues/222 + let realX = transformedVideoRect.origin.y + let realY = self.frame.size.width - transformedVideoRect.size.width - transformedVideoRect.origin.x + let realWidth = transformedVideoRect.size.height + let realHeight = transformedVideoRect.size.width + transformedVideoRect = CGRect(x: realX, y: realY, width: realWidth, height: realHeight) + + } else { + scaleVideoX = self.frame.width / videoWidth + scaleVideoY = self.frame.height / videoHeight + } + + captureSizeTransform = CGAffineTransform(scaleX: 1.0/scaleVideoX, y: 1.0/scaleVideoY) + + guard let _captureSizeTransform = captureSizeTransform else { + return + } + + let transformRect = transformedVideoRect.applying(_captureSizeTransform) + zxCapture.scanRect = transformRect + } +} + + +// MARK: - ZXCaptureDelegate +extension QRCodeReaderView: ZXCaptureDelegate { + + func captureCameraIsReady(_ capture: ZXCapture!) { + isScanning = true + } + + func captureResult(_ capture: ZXCapture!, result: ZXResult!) { + guard let zxResult = result, isScanning == true else { + return + } + + guard zxResult.barcodeFormat == kBarcodeFormatQRCode else { + return + } + + self.stopScanning() + + if let bytes = result.resultMetadata.object(forKey: kResultMetadataTypeByteSegments.rawValue) as? NSArray, + let byteArray = bytes.firstObject as? ZXByteArray { + + let data = Data(bytes: UnsafeRawPointer(byteArray.array), count: Int(byteArray.length)) + + self.didFoundData(data) + } + } +} diff --git a/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift b/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift index 4d35af705f..764744613b 100644 --- a/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift +++ b/Riot/Modules/QRCode/Reader/QRCodeReaderViewController.swift @@ -15,7 +15,6 @@ */ import UIKit -import ZXingObjC protocol QRCodeReaderViewControllerDelegate: AnyObject { func qrCodeReaderViewController(_ viewController: QRCodeReaderViewController, didFound payloadData: Data) @@ -40,10 +39,7 @@ final class QRCodeReaderViewController: UIViewController { private var theme: Theme! private var errorPresenter: MXKErrorPresentation! - private lazy var zxCapture: ZXCapture = ZXCapture() - private var captureSizeTransform: CGAffineTransform? - private var isScanning: Bool = false - private var isFirstApplyOrientation: Bool = false + private var qrCodeReaderView: QRCodeReaderView! // MARK: Public @@ -56,12 +52,7 @@ final class QRCodeReaderViewController: UIViewController { viewController.theme = ThemeService.shared().theme return viewController } - - deinit { - self.zxCapture.layer.removeFromSuperlayer() - self.zxCapture.hard_stop() - } - + // MARK: - Life cycle override func viewDidLoad() { @@ -92,40 +83,14 @@ final class QRCodeReaderViewController: UIViewController { return self.theme.statusBarStyle } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - guard isFirstApplyOrientation == false else { - return - } - - isFirstApplyOrientation = true - applyOrientation() - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - coordinator.animate(alongsideTransition: { (context) in - // do nothing - }, completion: { [weak self] (context) in - guard let self = self else { - return - } - self.applyOrientation() - }) - } - // MARK: - Public func startScanning() { - self.zxCapture.start() - isScanning = true + qrCodeReaderView.startScanning() } func stopScanning() { - self.zxCapture.stop() - isScanning = false + qrCodeReaderView.stopScanning() } // MARK: - Private @@ -145,94 +110,15 @@ final class QRCodeReaderViewController: UIViewController { } private func setupViews() { - self.setupQRCodeReaderView() - } - - private func setupQRCodeReaderView() { - zxCapture.delegate = self - zxCapture.camera = zxCapture.back() - - zxCapture.layer.frame = codeReaderContainerView.bounds - codeReaderContainerView.layer.addSublayer(zxCapture.layer) - } - - private func applyOrientation() { - - let orientation = UIApplication.shared.statusBarOrientation - let captureRotation: Double - let scanRectRotation: Double - - switch orientation { - case .portrait: - captureRotation = 0 - scanRectRotation = 90 - case .landscapeLeft: - captureRotation = 90 - scanRectRotation = 180 - case .landscapeRight: - captureRotation = 270 - scanRectRotation = 0 - case .portraitUpsideDown: - captureRotation = 180 - scanRectRotation = 270 - default: - captureRotation = 0 - scanRectRotation = 90 - } + let qrCodeReaderView = QRCodeReaderView() + qrCodeReaderView.didFoundData = qrCodeReader(didFound:) + self.qrCodeReaderView = qrCodeReaderView - applyRectOfInterest(orientation: orientation) - - let angleRadius = captureRotation / 180.0 * Double.pi - let captureTranform = CGAffineTransform(rotationAngle: CGFloat(angleRadius)) - - zxCapture.transform = captureTranform - zxCapture.rotation = CGFloat(scanRectRotation) - zxCapture.layer.frame = codeReaderContainerView.frame + self.codeReaderContainerView.vc_addSubViewMatchingParent(qrCodeReaderView) } - private func applyRectOfInterest(orientation: UIInterfaceOrientation) { - guard var transformedVideoRect = codeReaderContainerView?.frame, - let cameraSessionPreset = zxCapture.sessionPreset - else { return } - - var scaleVideoX, scaleVideoY: CGFloat - var videoHeight, videoWidth: CGFloat - - // Currently support only for 1920x1080 || 1280x720 - if cameraSessionPreset == AVCaptureSession.Preset.hd1920x1080.rawValue { - videoHeight = 1080.0 - videoWidth = 1920.0 - } else { - videoHeight = 720.0 - videoWidth = 1280.0 - } - - if orientation == UIInterfaceOrientation.portrait { - scaleVideoX = self.view.frame.width / videoHeight - scaleVideoY = self.view.frame.height / videoWidth - - // Convert CGPoint under portrait mode to map with orientation of image - // because the image will be cropped before rotate - // reference: https://github.com/TheLevelUp/ZXingObjC/issues/222 - let realX = transformedVideoRect.origin.y - let realY = self.view.frame.size.width - transformedVideoRect.size.width - transformedVideoRect.origin.x - let realWidth = transformedVideoRect.size.height - let realHeight = transformedVideoRect.size.width - transformedVideoRect = CGRect(x: realX, y: realY, width: realWidth, height: realHeight) - - } else { - scaleVideoX = self.view.frame.width / videoWidth - scaleVideoY = self.view.frame.height / videoHeight - } - - captureSizeTransform = CGAffineTransform(scaleX: 1.0/scaleVideoX, y: 1.0/scaleVideoY) - - guard let _captureSizeTransform = captureSizeTransform else { - return - } - - let transformRect = transformedVideoRect.applying(_captureSizeTransform) - zxCapture.scanRect = transformRect + private func qrCodeReader(didFound data: Data) { + self.delegate?.qrCodeReaderViewController(self, didFound: data) } // MARK: - Actions @@ -241,31 +127,3 @@ final class QRCodeReaderViewController: UIViewController { self.delegate?.qrCodeReaderViewControllerDidCancel(self) } } - -// MARK: - ZXCaptureDelegate -extension QRCodeReaderViewController: ZXCaptureDelegate { - - func captureCameraIsReady(_ capture: ZXCapture!) { - isScanning = true - } - - func captureResult(_ capture: ZXCapture!, result: ZXResult!) { - guard let zxResult = result, isScanning == true else { - return - } - - guard zxResult.barcodeFormat == kBarcodeFormatQRCode else { - return - } - - self.stopScanning() - - if let bytes = result.resultMetadata.object(forKey: kResultMetadataTypeByteSegments.rawValue) as? NSArray, - let byteArray = bytes.firstObject as? ZXByteArray { - - let data = Data(bytes: UnsafeRawPointer(byteArray.array), count: Int(byteArray.length)) - - self.delegate?.qrCodeReaderViewController(self, didFound: data) - } - } -} diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 219f674819..10b6737ed1 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1072,6 +1072,15 @@ - (BOOL)hasSameSenderAsBubbleCellData:(id)bubbleCe // We do not want to merge room create event cells with other cell types return NO; } + + if (self.tag == RoomBubbleCellDataTagPoll) { + MXEvent* event = self.events.firstObject; + + if (event) { + // m.poll.ended events should always show the sender information + return event.eventType != MXEventTypePollEnd; + } + } if (self.hasThreadRoot || bubbleCellData.hasThreadRoot) { diff --git a/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewModel.swift b/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewModel.swift index a692559d94..2a78d6ac07 100644 --- a/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewModel.swift +++ b/Riot/Modules/Room/CreationModal/RoomCreationEventsModal/RoomCreationEventsModalViewModel.swift @@ -62,7 +62,7 @@ final class RoomCreationEventsModalViewModel: RoomCreationEventsModalViewModelTy guard let summary = session.roomSummary(withRoomId: roomState.roomId) else { return nil } - return summary.displayname + return summary.displayName } var roomInfo: String? { diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index b1b43112c9..8d0d2808ac 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -760,7 +760,7 @@ - (void)updateViewControllerAppearanceOnRoomDataSourceState else { // set default title - self.navigationItem.title = roomDataSource.room.summary.displayname; + self.navigationItem.title = roomDataSource.room.summary.displayName; } // Show input tool bar @@ -780,7 +780,7 @@ - (void)updateViewControllerAppearanceOnRoomDataSourceState } else { - self.navigationItem.title = roomDataSource.room.summary.displayname; + self.navigationItem.title = roomDataSource.room.summary.displayName; } } else @@ -890,7 +890,7 @@ - (void)joinRoom:(void(^)(MXKRoomViewControllerJoinRoomResult result))completion } failure:^(NSError *error) { cancelIndicator(); - MXLogDebug(@"[MXKRoomVC] Failed to join room (%@)", self->roomDataSource.room.summary.displayname); + MXLogDebug(@"[MXKRoomVC] Failed to join room (%@)", self->roomDataSource.room.summary.displayName); [self processRoomJoinFailureWithError:error completion:completion]; }]; } diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m index aecfcf6652..3fe7516c49 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -366,11 +366,11 @@ - (void)updateMemberInfo switch (roomPowerLevel) { case RoomPowerLevelAdmin: - self.roomMemberPowerLevelLabel.text = [VectorL10n roomMemberPowerLevelAdminIn:self.mxRoom.summary.displayname]; + self.roomMemberPowerLevelLabel.text = [VectorL10n roomMemberPowerLevelAdminIn:self.mxRoom.summary.displayName]; self.roomMemberPowerLevelContainerView.hidden = NO; break; case RoomPowerLevelModerator: - self.roomMemberPowerLevelLabel.text = [VectorL10n roomMemberPowerLevelModeratorIn:self.mxRoom.summary.displayname]; + self.roomMemberPowerLevelLabel.text = [VectorL10n roomMemberPowerLevelModeratorIn:self.mxRoom.summary.displayName]; self.roomMemberPowerLevelContainerView.hidden = NO; break; default: diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index a345077118..badc664904 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -31,6 +31,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { private let parentSpaceId: String? private let initialSection: RoomInfoSection private let dismissOnCancel: Bool + private let canAddParticipants: Bool private weak var roomSettingsViewController: RoomSettingsViewController? private lazy var segmentedViewController: SegmentedViewController = { @@ -43,6 +44,8 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { participants.parentSpaceId = self.parentSpaceId participants.delegate = self participants.screenTracker = AnalyticsScreenTracker(screen: .roomMembers) + participants.showInviteUserFab = self.canAddParticipants + let files = RoomFilesViewController() files.finalizeInit() @@ -105,6 +108,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { self.room = parameters.room self.parentSpaceId = parameters.parentSpaceId self.initialSection = parameters.initialSection + self.canAddParticipants = parameters.canAddParticipants self.dismissOnCancel = parameters.dismissOnCancel } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift index c1c46fab4a..2478922d66 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorParameters.swift @@ -33,12 +33,14 @@ class RoomInfoCoordinatorParameters: NSObject { let parentSpaceId: String? let initialSection: RoomInfoSection let dismissOnCancel: Bool + let canAddParticipants: Bool - init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, dismissOnCancel: Bool) { + init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, canAddParticipants: Bool = true, dismissOnCancel: Bool) { self.session = session self.room = room self.parentSpaceId = parentSpaceId self.initialSection = initialSection + self.canAddParticipants = canAddParticipants self.dismissOnCancel = dismissOnCancel super.init() } @@ -50,4 +52,8 @@ class RoomInfoCoordinatorParameters: NSObject { convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection) { self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: initialSection, dismissOnCancel: false) } + + convenience init(session: MXSession, room: MXRoom, parentSpaceId: String?, initialSection: RoomInfoSection, canAddParticipants: Bool) { + self.init(session: session, room: room, parentSpaceId: parentSpaceId, initialSection: initialSection, canAddParticipants: canAddParticipants, dismissOnCancel: false) + } } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift index 52d0b57948..572754251a 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift @@ -39,7 +39,7 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType { let basicInfoViewData = RoomInfoBasicViewData(avatarUrl: room.summary.avatar, mediaManager: session.mediaManager, roomId: room.roomId, - roomDisplayName: room.summary.displayname, + roomDisplayName: room.summary.displayName, mainRoomAlias: room.summary.aliases?.first, roomTopic: room.summary.topic, encryptionImage: encryptionImage, diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 14bab4c051..072a882a69 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -125,6 +125,8 @@ extern NSTimeInterval const kResizeComposerAnimationDuration; @property (nonatomic, strong, nullable) ComposerLinkActionBridgePresenter *composerLinkActionBridgePresenter; +@property (weak, nonatomic, nullable) UIViewController *waitingOtherParticipantViewController; +@property (nonatomic) BOOL isWaitingForOtherParticipants; /** Retrieve the live data source in cases where the timeline is not live. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index d4fdb6bd71..2e641916a7 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -185,6 +185,9 @@ @interface RoomViewController () = [powerLevels minimumPowerLevelForSendingEventAsMessage:kMXEventTypeStringRoomMessage]); BOOL isRoomObsolete = self.roomDataSource.roomState.isObsolete; - BOOL isResourceLimitExceeded = [self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded]; + BOOL isResourceLimitExceeded = [self.roomDataSource.mxSession.syncError.errcode isEqualToString:kMXErrCodeStringResourceLimitExceeded]; - if (isRoomObsolete || isResourceLimitExceeded) + if (isRoomObsolete || isResourceLimitExceeded || _isWaitingForOtherParticipants) { roomInputToolbarViewClass = nil; shouldDismissContextualMenu = YES; @@ -1532,6 +1542,8 @@ - (void)destroy [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeSentStateNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeIdentifierNotification object:nil]; + [self waitForOtherParticipant:NO]; + [super destroy]; } @@ -1638,6 +1650,57 @@ - (BOOL)shouldShowLiveLocationSharingBannerView return self.customizedRoomDataSource.isCurrentUserSharingActiveLocation; } +#pragma mark - Wait for 3rd party invitee + +- (void)setIsWaitingForOtherParticipants:(BOOL)isWaitingForOtherParticipants +{ + if (_isWaitingForOtherParticipants == isWaitingForOtherParticipants) + { + return; + } + + _isWaitingForOtherParticipants = isWaitingForOtherParticipants; + [self updateRoomInputToolbarViewClassIfNeeded]; + + if (_isWaitingForOtherParticipants) + { + if (self->roomMemberEventListener == nil) + { + MXWeakify(self); + self->roomMemberEventListener = [self.roomDataSource.room listenToEventsOfTypes:@[kMXEventTypeStringRoomMember] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) { + MXStrongifyAndReturnIfNil(self); + if (direction != MXTimelineDirectionForwards) + { + return; + } + [self refreshWaitForOtherParticipantsState]; + }]; + } + } + else + { + if (self->roomMemberEventListener != nil) + { + [self.roomDataSource.room removeListener:self->roomMemberEventListener]; + self->roomMemberEventListener = nil; + } + } +} + +- (BOOL)shouldWaitForOtherParticipants +{ + MXRoomState *roomState = self.roomDataSource.roomState; + BOOL isDirect = self.roomDataSource.room.isDirect; + + // Wait for the other participant only if it is a direct encrypted room with only one member waiting for a third party guest. + return (isDirect && roomState.isEncrypted && roomState.membersCount.members == 1 && roomState.thirdPartyInvites.count > 0); +} + +- (void)refreshWaitForOtherParticipantsState +{ + [self waitForOtherParticipant:self.shouldWaitForOtherParticipants]; +} + #pragma mark - Internals - (UIBarButtonItem *)videoCallBarButtonItem @@ -1948,7 +2011,7 @@ - (void)refreshRoomTitle [self refreshMissedDiscussionsCount:YES]; - if (RiotSettings.shared.enableThreads) + if (RiotSettings.shared.enableThreads && !_isWaitingForOtherParticipants) { if (self.roomDataSource.threadId) { @@ -2260,8 +2323,8 @@ - (void)showRoomInfo - (void)showRoomInfoWithInitialSection:(RoomInfoSection)roomInfoSection animated:(BOOL)animated { - RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection]; - + RoomInfoCoordinatorParameters *parameters = [[RoomInfoCoordinatorParameters alloc] initWithSession:self.roomDataSource.mxSession room:self.roomDataSource.room parentSpaceId:self.parentSpaceId initialSection:roomInfoSection canAddParticipants: !self.isWaitingForOtherParticipants]; + self.roomInfoCoordinatorBridgePresenter = [[RoomInfoCoordinatorBridgePresenter alloc] initWithParameters:parameters]; self.roomInfoCoordinatorBridgePresenter.delegate = self; @@ -7450,7 +7513,7 @@ - (void)updateThreadListBarButtonBadgeWith:(MXThreadingService *)service - (void)updateThreadListBarButtonItem:(UIBarButtonItem *)barButtonItem with:(MXThreadingService *)service { - if (!service) + if (!service || _isWaitingForOtherParticipants) { return; } diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 561cc382a8..a177281f36 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -251,6 +251,50 @@ extension RoomViewController { composerLinkActionBridgePresenter = presenter presenter.present(from: self, animated: true) } + + @objc func showWaitingOtherParticipantHeader() { + let controller = VectorHostingController(rootView: RoomWaitingForMembers()) + guard let headerView = controller.view else { + return + } + self.waitingOtherParticipantViewController = controller + self.addChild(controller) + + let containerView = UIView() + containerView.translatesAutoresizingMaskIntoConstraints = false + headerView.translatesAutoresizingMaskIntoConstraints = false + containerView.vc_addSubViewMatchingParent(headerView, withInsets: UIEdgeInsets(top: 9, left: 9, bottom: -9, right: -9)) + + self.bubblesTableView.tableHeaderView = containerView + NSLayoutConstraint.activate([ + containerView.centerXAnchor.constraint(equalTo: self.bubblesTableView.centerXAnchor), + containerView.widthAnchor.constraint(equalTo: self.bubblesTableView.widthAnchor), + containerView.topAnchor.constraint(equalTo: self.bubblesTableView.topAnchor) + ]) + controller.didMove(toParent: self) + + self.bubblesTableView.tableHeaderView?.layoutIfNeeded() + } + + @objc func hideWaitingOtherParticipantHeader() { + guard let waitingOtherParticipantViewController else { + return + } + waitingOtherParticipantViewController.removeFromParent() + self.bubblesTableView.tableHeaderView = nil + waitingOtherParticipantViewController.didMove(toParent: nil) + self.waitingOtherParticipantViewController = nil + } + + @objc func waitForOtherParticipant(_ wait: Bool) { + self.isWaitingForOtherParticipants = wait + if wait { + showWaitingOtherParticipantHeader() + } else { + hideWaitingOtherParticipantHeader() + } + } + } // MARK: - Private Helpers diff --git a/Riot/Modules/Room/TimelineCells/Call/Group/RoomGroupCallStatusCell.swift b/Riot/Modules/Room/TimelineCells/Call/Group/RoomGroupCallStatusCell.swift index 30b444829d..3ff48f568f 100644 --- a/Riot/Modules/Room/TimelineCells/Call/Group/RoomGroupCallStatusCell.swift +++ b/Riot/Modules/Room/TimelineCells/Call/Group/RoomGroupCallStatusCell.swift @@ -237,13 +237,13 @@ class RoomGroupCallStatusCell: RoomCallBaseCell { TimeInterval(widgetEvent.age)/MSEC_PER_SEC < Constants.secondsToDisplayAnswerDeclineOptions { if JitsiService.shared.isWidgetDeclined(withId: widgetId) { - innerContentView.callerNameLabel.text = room.summary.displayname + innerContentView.callerNameLabel.text = room.summary.displayName room.summary.setRoomAvatarImageIn(innerContentView.avatarImageView) viewState = .declined statusText = VectorL10n.eventFormatterCallYouDeclined } else { - innerContentView.callerNameLabel.text = VectorL10n.eventFormatterGroupCallIncoming(bubbleCellData.senderDisplayName, room.summary.displayname) + innerContentView.callerNameLabel.text = VectorL10n.eventFormatterGroupCallIncoming(bubbleCellData.senderDisplayName, room.summary.displayName) innerContentView.avatarImageView.setImageURI(bubbleCellData.senderAvatarUrl, withType: nil, @@ -257,7 +257,7 @@ class RoomGroupCallStatusCell: RoomCallBaseCell { statusText = nil } } else { - innerContentView.callerNameLabel.text = room.summary.displayname + innerContentView.callerNameLabel.text = room.summary.displayName room.summary.setRoomAvatarImageIn(innerContentView.avatarImageView) } diff --git a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift index c71fb36530..9bccbddf96 100644 --- a/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift +++ b/Riot/Modules/Room/TimelineCells/RoomCreationIntro/RoomCreationIntroCell.swift @@ -189,7 +189,7 @@ class RoomCreationIntroCell: MXKRoomBubbleTableViewCell { discussionType = .room(topic: roomSummary.topic, canInvitePeople: bubbleData.canInvitePeople) } - let displayName = roomSummary.displayname ?? "" + let displayName = roomSummary.displayName ?? "" let roomAvatarViewData = RoomAvatarViewData(roomId: roomId, displayName: displayName, diff --git a/Riot/Modules/Room/Views/Title/RoomTitleView.m b/Riot/Modules/Room/Views/Title/RoomTitleView.m index b07f928d9b..48cbb5a3b4 100644 --- a/Riot/Modules/Room/Views/Title/RoomTitleView.m +++ b/Riot/Modules/Room/Views/Title/RoomTitleView.m @@ -85,7 +85,7 @@ -(void)customizeViewRendering [super customizeViewRendering]; self.backgroundColor = UIColor.clearColor; - self.displayNameTextField.textColor = (self.mxRoom.summary.displayname.length ? ThemeService.shared.theme.textPrimaryColor : ThemeService.shared.theme.textSecondaryColor); + self.displayNameTextField.textColor = (self.mxRoom.summary.displayName.length ? ThemeService.shared.theme.textPrimaryColor : ThemeService.shared.theme.textSecondaryColor); self.typingLabel.textColor = ThemeService.shared.theme.textSecondaryColor; self.dotView.backgroundColor = ThemeService.shared.theme.warningColor; self.missedDiscussionsBadgeLabel.textColor = ThemeService.shared.theme.tintColor; @@ -121,7 +121,7 @@ - (void)refreshDisplay [self.presenceIndicatorView stopListeningPresenceUpdates]; } - self.displayNameTextField.text = self.mxRoom.summary.displayname; + self.displayNameTextField.text = self.mxRoom.summary.displayName; if (!self.displayNameTextField.text.length) { self.displayNameTextField.text = [VectorL10n roomDisplaynameEmptyRoom]; diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 0a9283a3c4..79d96b5481 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -48,7 +48,7 @@ import MediaPlayer didSet { // set avatar placeholder for now roomAvatar = AvatarGenerator.generateAvatar(forMatrixItem: currentRoomSummary?.roomId, - withDisplayName: currentRoomSummary?.displayname, + withDisplayName: currentRoomSummary?.displayName, size: Constants.roomAvatarImageSize.width, andFontSize: Constants.roomAvatarFontSize) diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift index d1aabdea13..c0cb402f00 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewModel.swift @@ -237,7 +237,7 @@ final class ShowDirectoryViewModel: NSObject, ShowDirectoryViewModelType { } private func roomCellViewModel(with room: MXRoom) -> DirectoryRoomTableViewCellVM { - let displayName = room.summary.displayname + let displayName = room.summary.displayName let joinedMembersCount = Int(room.summary.membersCount.joined) let topic = MXTools.stripNewlineCharacters(room.summary.topic) let isJoined = room.summary.membership == .join || room.summary.membershipTransitionState == .joined diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index ea8b57b228..0236791255 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -588,7 +588,7 @@ - (void)updateSections if (BuildSettings.settingsScreenShowLabSettings) { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; - if (MXSDKOptions.sharedInstance.isCryptoSDKAvailable) + if ([CryptoSDKFeature.shared canManuallyEnableForUserId:self.mainSession.myUserId]) { [sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK]; } @@ -2589,20 +2589,17 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell = labelAndSwitchCell; } - else + else if (row == LABS_ENABLE_CRYPTO_SDK) { - if (row == LABS_ENABLE_CRYPTO_SDK) - { - MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK; - labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk; - labelAndSwitchCell.mxkSwitch.on = isEnabled; - [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled]; - labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; - - cell = labelAndSwitchCell; - } + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK; + labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk; + labelAndSwitchCell.mxkSwitch.on = isEnabled; + [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled]; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(enableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; } } else if (section == SECTION_TAG_SECURITY) @@ -3391,10 +3388,7 @@ - (void)enableCryptoSDKFeature:(UISwitch *)sender }]]; [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - - [CryptoSDKConfiguration.shared enable]; - [Analytics.shared trackCryptoSDKEnabled]; - + [CryptoSDKFeature.shared enable]; [[AppDelegate theDelegate] reloadMatrixSessions:YES]; }]]; diff --git a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift index 21140abb5c..362bfe14dc 100644 --- a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift +++ b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift @@ -102,7 +102,7 @@ class SpaceDetailViewModel: SpaceDetailViewModelType { } let parameters = SpaceDetailLoadedParameters(spaceId: space.spaceId, - displayName: summary.displayname, + displayName: summary.displayName, topic: summary.topic, avatarUrl: summary.avatar, joinRule: nil, @@ -130,7 +130,7 @@ class SpaceDetailViewModel: SpaceDetailViewModelType { }) let parameters = SpaceDetailLoadedParameters(spaceId: space.spaceId, - displayName: summary.displayname, + displayName: summary.displayName, topic: summary.topic, avatarUrl: summary.avatar, joinRule: joinRule, diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift index 428cf83024..762593db7e 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift @@ -248,9 +248,9 @@ final class SpaceListViewModel: SpaceListViewModelType { var invites: [SpaceListItemViewData] = [] var spaces: [SpaceListItemViewData] = [] session.spaceService.rootSpaceSummaries.forEach { summary in - let avatarViewData = AvatarViewData(matrixItemId: summary.roomId, displayName: summary.displayname, avatarUrl: summary.avatar, mediaManager: session.mediaManager, fallbackImage: .matrixItem(summary.roomId, summary.displayname)) + let avatarViewData = AvatarViewData(matrixItemId: summary.roomId, displayName: summary.displayName, avatarUrl: summary.avatar, mediaManager: session.mediaManager, fallbackImage: .matrixItem(summary.roomId, summary.displayName)) let notificationState = session.spaceService.notificationCounter.notificationState(forSpaceWithId: summary.roomId) - let viewData = SpaceListItemViewData(spaceId: summary.roomId, title: summary.displayname, + let viewData = SpaceListItemViewData(spaceId: summary.roomId, title: summary.displayName, avatarViewData: avatarViewData, isInvite: summary.membership == .invite, notificationCount: notificationState?.groupMissedDiscussionsCount ?? 0, diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift index ef3290ec70..91a96197b9 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift @@ -142,7 +142,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { private func renderLoaded(space: MXSpace) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.mxRoom = space.room - if let spaceName = space.summary?.displayname { + if let spaceName = space.summary?.displayName { self.titleView.breadcrumbView.breadcrumbs = [spaceName] } else { self.titleView.breadcrumbView.breadcrumbs = [] diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift index bf4e03aa6b..53e463dc44 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift @@ -101,7 +101,7 @@ class SpaceMenuPresenter: NSObject { } private func showLeaveSpace() { - let name = session.spaceService.getSpace(withId: spaceId)?.summary?.displayname ?? VectorL10n.spaceTag + let name = session.spaceService.getSpace(withId: spaceId)?.summary?.displayName ?? VectorL10n.spaceTag let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle, selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms, diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift index 1d2a54eb65..4caabe2a9a 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewController.swift @@ -140,9 +140,9 @@ class SpaceMenuViewController: UIViewController { return } - let avatarViewData = AvatarViewData(matrixItemId: summary.roomId, displayName: summary.displayname, avatarUrl: summary.avatar, mediaManager: self.session.mediaManager, fallbackImage: .matrixItem(summary.roomId, summary.displayname)) + let avatarViewData = AvatarViewData(matrixItemId: summary.roomId, displayName: summary.displayName, avatarUrl: summary.avatar, mediaManager: self.session.mediaManager, fallbackImage: .matrixItem(summary.roomId, summary.displayName)) - self.titleLabel.text = space.summary?.displayname + self.titleLabel.text = space.summary?.displayName // TODO: display members instead once done on android // self.subtitleLabel.text = space.membersId.count == 1 ? VectorL10n.roomTitleOneMember : // VectorL10n.roomTitleMembers("\(space.membersId.count)") diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift index fbda4f49a2..d445f29afc 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift @@ -60,7 +60,7 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType { func start() { - let rootCoordinator = self.createShowSpaceExploreRoomCoordinator(session: self.session, spaceId: self.spaceId, spaceName: self.session.spaceService.getSpace(withId: self.spaceId)?.summary?.displayname) + let rootCoordinator = self.createShowSpaceExploreRoomCoordinator(session: self.session, spaceId: self.spaceId, spaceName: self.session.spaceService.getSpace(withId: self.spaceId)?.summary?.displayName) rootCoordinator.start() diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index c70bbdc833..f2bf8ff538 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -35,6 +35,9 @@ @interface StartChatViewController () 0) + { + return NO; + } + + // Otherwise, we should be able to add this participant + return YES; +} + +- (void)showAllowOnlyOneInvitByEmailAllowedHeaderView:(BOOL)visible +{ + if (visible) + { + if (!self.onlyOneEmailInvitationView) + { + UIView *headerView = [[UIView alloc] initWithFrame: CGRectZero]; + headerView.translatesAutoresizingMaskIntoConstraints = NO; + + UILabel *label = [[UILabel alloc] initWithFrame: CGRectZero]; + label.numberOfLines = 0; + label.textColor = ThemeService.shared.theme.textSecondaryColor; + label.font = [UIFont systemFontOfSize:14 weight:UIFontWeightLight]; + label.adjustsFontSizeToFitWidth = YES; + + label.text = VectorL10n.roomCreationOnlyOneEmailInvite; + label.translatesAutoresizingMaskIntoConstraints = NO; + [headerView addSubview:label]; + + [NSLayoutConstraint activateConstraints:@[ + [label.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor constant:16], + [label.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor constant:-16], + [label.topAnchor constraintEqualToAnchor:headerView.topAnchor constant:8], + [label.bottomAnchor constraintEqualToAnchor:headerView.bottomAnchor constant:-8], + ]]; + [label setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + [headerView setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + + self.onlyOneEmailInvitationView = headerView; + self.contactsTableView.tableHeaderView = self.onlyOneEmailInvitationView; + + [NSLayoutConstraint activateConstraints:@[ + [headerView.leadingAnchor constraintEqualToAnchor:self.contactsTableView.safeAreaLayoutGuide.leadingAnchor], + [headerView.trailingAnchor constraintEqualToAnchor:self.contactsTableView.safeAreaLayoutGuide.trailingAnchor] + ]]; + [self.contactsTableView.tableHeaderView layoutIfNeeded]; + } + } + else if (self.onlyOneEmailInvitationView != nil) + { + if (self.contactsTableView.tableHeaderView == self.onlyOneEmailInvitationView) + { + self.contactsTableView.tableHeaderView = nil; + } + self.onlyOneEmailInvitationView = nil; + } } - (void)showInviteFriendsFromSourceView:(UIView*)sourceView @@ -367,6 +476,14 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N if (_isAddParticipantSearchBarEditing) { cell = [contactsDataSource tableView:tableView cellForRowAtIndexPath:indexPath]; + MXKContact* contact = [contactsDataSource contactAtIndexPath:indexPath]; + if (![self canAddParticipant:contact]) + { + // Prevent to add it + cell.contentView.alpha = 0.5; + cell.userInteractionEnabled = NO; + cell.accessoryView = nil; + } } else if (indexPath.section == participantsSection) { @@ -533,7 +650,7 @@ - (IBAction)onButtonPressed:(id)sender // Prepare the invited participant data NSMutableArray *inviteArray = [NSMutableArray array]; - NSMutableArray *invite3PIDArray = [NSMutableArray array]; + NSMutableArray *invite3PIDArray = [NSMutableArray array]; // Check whether some users must be invited for (MXKContact *contact in participants) @@ -594,7 +711,7 @@ - (IBAction)onButtonPressed:(id)sender // Is it a direct chat? BOOL isDirect = ((inviteArray.count + invite3PIDArray.count == 1) ? YES : NO); - + // In case of a direct chat with only one user id, we open the first available direct chat // or creates a new one (if it doesn't exist). if (isDirect && inviteArray.count) @@ -606,6 +723,19 @@ - (IBAction)onButtonPressed:(id)sender } else { + // We don't want to create a new direct room for a 3rd party invite if we already have one + NSString *first3rdPartyInvitee = invite3PIDArray.firstObject.address; + if (isDirect && first3rdPartyInvitee) + { + MXRoom *existingRoom = [self.mainSession directJoinedRoomWithUserId:first3rdPartyInvitee]; + if (existingRoom) + { + [self stopActivityIndicator]; + [[AppDelegate theDelegate] showRoom:existingRoom.roomId andEventId:nil withMatrixSession:self.mainSession]; + return; + } + } + // Ensure direct chat are created with equal ops on both sides (the trusted_private_chat preset) MXRoomPreset preset = (isDirect ? kMXRoomPresetTrustedPrivateChat : nil); @@ -635,7 +765,7 @@ - (IBAction)onButtonPressed:(id)sender roomCreationParameters.isDirect = isDirect; roomCreationParameters.preset = preset; - if (canEnableE2E && roomCreationParameters.invite3PIDArray == nil) + if (canEnableE2E) { roomCreationParameters.initialStateEvents = @[ [MXRoomCreationParameters initialStateEventForEncryptionWithAlgorithm:kMXCryptoMegolmAlgorithm @@ -644,6 +774,9 @@ - (IBAction)onButtonPressed:(id)sender self->roomCreationRequest = [self.mainSession createRoomWithParameters:roomCreationParameters success:^(MXRoom *room) { + // Update the room summary + [room.summary resetRoomStateData]; + self->roomCreationRequest = nil; [self stopActivityIndicator]; @@ -702,8 +835,28 @@ - (void)refreshSearchBarItemsColor:(UISearchBar *)searchBar - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { - [contactsDataSource searchWithPattern:searchText forceReset:NO]; + self->currentSearch = searchText; + if (searchText != nil && searchText.length > 0) + { + MXKContact *contact = nil; + if ([MXTools isMatrixUserIdentifier:searchText]) + { + contact = [[MXKContact alloc] initMatrixContactWithDisplayName:searchText andMatrixID:searchText]; + + } + else if ([MXTools isEmailAddress:searchText]) + { + contact = [[MXKContact alloc] initContactWithDisplayName:searchText emails:nil phoneNumbers:nil andThumbnail:nil]; + } + + [self showAllowOnlyOneInvitByEmailAllowedHeaderView: ![self canAddParticipant:contact]]; + } + else + { + [self showAllowOnlyOneInvitByEmailAllowedHeaderView:NO]; + } + [contactsDataSource searchWithPattern:searchText forceReset:NO]; self.contactsAreFilteredWithSearch = searchText.length ? YES : NO; } @@ -718,6 +871,7 @@ - (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { searchBar.text = nil; + self->currentSearch = nil; self.isAddParticipantSearchBarEditing = NO; // Reset filtering @@ -725,6 +879,8 @@ - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar // Leave search [searchBar resignFirstResponder]; + + [self showAllowOnlyOneInvitByEmailAllowedHeaderView:NO]; } #pragma mark - ContactsTableViewControllerDelegate @@ -763,14 +919,14 @@ - (void)contactsTableViewController:(ContactsTableViewController *)contactsTable } } - if (contact) + if ([self canAddParticipant:contact]) { // Update here the mutable list of participants [participants addObject:contact]; + + // Refresh display by leaving search session + [self searchBarCancelButtonClicked:_searchBarView]; } - - // Refresh display by leaving search session - [self searchBarCancelButtonClicked:_searchBarView]; } #pragma mark - InviteFriendsHeaderViewDelegate diff --git a/Riot/Modules/StartChat/StartChatViewController.xib b/Riot/Modules/StartChat/StartChatViewController.xib index 958aaf9045..eb4ea3a6a3 100644 --- a/Riot/Modules/StartChat/StartChatViewController.xib +++ b/Riot/Modules/StartChat/StartChatViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -13,6 +13,7 @@ + diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index a9c810c93f..02d09c7819 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -607,14 +607,14 @@ - (void)filterRoomsWithParentId:(NSString*)roomParentId inMatrixSession:(MXSession*)mxSession { if (roomParentId) { - NSString *parentName = [mxSession roomSummaryWithRoomId:roomParentId].displayname; + NSString *parentName = [mxSession roomSummaryWithRoomId:roomParentId].displayName; NSMutableArray *breadcrumbs = [[NSMutableArray alloc] initWithObjects:parentName, nil]; MXSpace *firstRootAncestor = roomParentId ? [mxSession.spaceService firstRootAncestorForRoomWithId:roomParentId] : nil; NSString *rootName = nil; if (firstRootAncestor) { - rootName = [mxSession roomSummaryWithRoomId:firstRootAncestor.spaceId].displayname; + rootName = [mxSession roomSummaryWithRoomId:firstRootAncestor.spaceId].displayName; [breadcrumbs insertObject:rootName atIndex:0]; } titleView.breadcrumbView.breadcrumbs = breadcrumbs; diff --git a/Riot/target.yml b/Riot/target.yml index b0fc131fb2..8cd537cd33 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -39,6 +39,7 @@ targets: - target: RiotNSE - target: DesignKit - target: CommonKit + - package: AnalyticsEvents - package: Mapbox - package: OrderedCollections - package: SwiftOGG diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index f0c0b1fab6..0c7257dba9 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -200,11 +200,14 @@ class NotificationService: UNNotificationServiceExtension { MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: BEFORE") self.logMemory() - NotificationService.backgroundSyncService = MXBackgroundSyncService(withCredentials: userAccount.mxCredentials, persistTokenDataHandler: { persistTokenDataHandler in - MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) - }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in - userAccount.handleUnauthenticatedWithError(error, isSoftLogout: softLogout, isRefreshTokenAuth: refreshTokenAuth, andCompletion: completion) - }) + NotificationService.backgroundSyncService = MXBackgroundSyncService( + withCredentials: userAccount.mxCredentials, + isCryptoSDKEnabled: isCryptoSDKEnabled, + persistTokenDataHandler: { persistTokenDataHandler in + MXKAccountManager.shared().readAndWriteCredentials(persistTokenDataHandler) + }, unauthenticatedHandler: { error, softLogout, refreshTokenAuth, completion in + userAccount.handleUnauthenticatedWithError(error, isSoftLogout: softLogout, isRefreshTokenAuth: refreshTokenAuth, andCompletion: completion) + }) MXLog.debug("[NotificationService] setup: MXBackgroundSyncService init: AFTER") self.logMemory() } @@ -219,10 +222,10 @@ class NotificationService: UNNotificationServiceExtension { /// Determine whether we have switched from using crypto v1 to v2 or vice versa which will require /// rebuilding `MXBackgroundSyncService` private func hasChangedCryptoSDK() -> Bool { - guard isCryptoSDKEnabled != RiotSettings.shared.enableCryptoSDK else { + guard isCryptoSDKEnabled != MXSDKOptions.sharedInstance().enableCryptoSDK else { return false } - isCryptoSDKEnabled = RiotSettings.shared.enableCryptoSDK + isCryptoSDKEnabled = MXSDKOptions.sharedInstance().enableCryptoSDK return true } @@ -238,7 +241,7 @@ class NotificationService: UNNotificationServiceExtension { // If a room summary is available, use the displayname for the best attempt title. guard let roomSummary = NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId) else { return } - guard let roomDisplayName = roomSummary.displayname else { return } + guard let roomDisplayName = roomSummary.displayName else { return } bestAttemptContents[eventId]?.title = roomDisplayName // At this stage we don't know the message type, so leave the body as set in didReceive. @@ -380,7 +383,7 @@ class NotificationService: UNNotificationServiceExtension { var ignoreBadgeUpdate = false var threadIdentifier: String? = roomId let currentUserId = account.mxCredentials.userId - let roomDisplayName = roomSummary?.displayname + let roomDisplayName = roomSummary?.displayName let pushRule = NotificationService.backgroundSyncService.pushRule(matching: event, roomState: roomState) // if the push rule must not be notified we complete and return diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml index 21c5f2864b..87aedd471d 100644 --- a/RiotNSE/target.yml +++ b/RiotNSE/target.yml @@ -32,6 +32,7 @@ targets: type: app-extension dependencies: + - package: AnalyticsEvents - package: DeviceKit - package: DTCoreText @@ -45,6 +46,7 @@ targets: - path: ../Config/BuildSettings.swift - path: ../Riot/Utils/DataProtectionHelper.swift - path: ../Config/CommonConfiguration.swift + - path: ../Riot/Experiments/ - path: ../Riot/Managers/PushNotification/PushNotificationStore.swift - path: ../Riot/Modules/SetPinCode/PinCodePreferences.swift - path: ../Riot/Managers/KeyValueStorage/Extensions/Keychain.swift diff --git a/RiotShareExtension/Shared/ShareDataSource.m b/RiotShareExtension/Shared/ShareDataSource.m index e097b5da7a..8133e67f19 100644 --- a/RiotShareExtension/Shared/ShareDataSource.m +++ b/RiotShareExtension/Shared/ShareDataSource.m @@ -141,7 +141,7 @@ - (void)searchWithPatterns:(NSArray *)patternsList { for (NSString* pattern in patternsList) { - if (cellData.roomSummary.displayname && [cellData.roomSummary.displayname rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound) + if (cellData.roomSummary.displayName && [cellData.roomSummary.displayName rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound) { [self.visibleRoomCellDatas addObject:cellData]; break; diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index 8601ce85f1..eaf51ce3c8 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -32,6 +32,7 @@ targets: type: app-extension dependencies: + - package: AnalyticsEvents - package: DeviceKit - package: DTCoreText @@ -52,6 +53,7 @@ targets: - path: ../Riot/Categories/MXRoom+Riot.m - path: ../Config/Configurable.swift - path: ../Config/CommonConfiguration.swift + - path: ../Riot/Experiments/ - path: ../Riot/Utils/UserNameColorGenerator.swift - path: ../Riot/Categories/MXRoomSummary+Riot.m - path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift diff --git a/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift b/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift index ac7a7ebbc4..60589f9d82 100644 --- a/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift +++ b/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift @@ -25,6 +25,6 @@ extension MXRoom: Avatarable { } var displayName: String? { - summary.displayname + summary.displayName } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift index e6e93ccca8..7c0adedd10 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift @@ -42,7 +42,8 @@ struct LiveLocationSharingViewerViewState: BindableState { /// Live location list items var listItemsViewData: [LiveLocationListItemViewData] - var showsUserLocation = false + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user + var showsUserLocationMode: ShowUserLocationMode = .hide var isCurrentUserShared: Bool { listItemsViewData.contains { $0.isCurrentUser } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift index 6cc99e5a01..6a3931b581 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift @@ -211,6 +211,14 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType return } + /* + if the map is currently following the current user's location, + we want to switch back to only showing the marker, + so the the highlighted shared location can be centered + */ + if state.showsUserLocationMode == .follow { + state.showsUserLocationMode = .show + } state.highlightedAnnotation = foundUserAnnotation } @@ -234,7 +242,7 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType private func showsCurrentUserLocation() { if liveLocationSharingViewerService.requestAuthorizationIfNeeded() { - state.showsUserLocation = true + state.showsUserLocationMode = .follow } else { state.errorSubject.send(.invalidLocationAuthorization) } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift index 37ee71a61a..c20df0e961 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift @@ -40,8 +40,10 @@ class LiveLocationSharingViewerViewModelTests: XCTestCase { let service = MockLiveLocationSharingViewerService(currentUserSharingLocation: false) let viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.defaultTileServerMapStyleURL, service: service) XCTAssertFalse(viewModel.context.viewState.isCurrentUserShared) - XCTAssertFalse(viewModel.context.viewState.showsUserLocation) + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .hide) viewModel.context.send(viewAction: .showUserLocation) - XCTAssertTrue(viewModel.context.viewState.showsUserLocation) + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .follow) + viewModel.context.send(viewAction: .tapListItem("@bob:matrix.org")) + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .show) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift index 2e1e1c0127..e0ac8ac767 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -39,7 +39,7 @@ struct LiveLocationSharingViewer: View { annotations: viewModel.viewState.annotations, highlightedAnnotation: viewModel.viewState.highlightedAnnotation, userAvatarData: nil, - showsUserLocation: viewModel.viewState.showsUserLocation, + showsUserLocationMode: viewModel.viewState.showsUserLocationMode, userAnnotationCanShowCallout: true, userLocation: Binding.constant(nil), mapCenterCoordinate: Binding.constant(nil), diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift index 31165396d4..90b9e5d645 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift @@ -18,6 +18,15 @@ import Combine import Mapbox import SwiftUI +/* + Behavior mode of the current user's location, can be hidden, only shown and shown following the user + */ +enum ShowUserLocationMode { + case follow + case show + case hide +} + struct LocationSharingMapView: UIViewRepresentable { // MARK: - Constants @@ -39,8 +48,8 @@ struct LocationSharingMapView: UIViewRepresentable { /// Current user avatar data, used to replace current location annotation view with the user avatar let userAvatarData: AvatarInputProtocol? - /// True to indicate to show and follow current user location - var showsUserLocation = false + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user + var showsUserLocationMode: ShowUserLocationMode = .hide /// True to indicate that a touch on user annotation can show a callout var userAnnotationCanShowCallout = false @@ -75,14 +84,23 @@ struct LocationSharingMapView: UIViewRepresentable { mapView.vc_removeAllAnnotations() mapView.addAnnotations(annotations) - if let highlightedAnnotation = highlightedAnnotation, !showsUserLocation { - mapView.setCenter(highlightedAnnotation.coordinate, zoomLevel: Constants.mapZoomLevel, animated: false) + /* + if there is an highlighted annotation, + and the current user's location it's either hidden or only shown, + we can center to the highlighted annotation + */ + if let highlightedAnnotation = highlightedAnnotation, showsUserLocationMode != .follow { + mapView.setCenter(highlightedAnnotation.coordinate, zoomLevel: Constants.mapZoomLevel, animated: true) } - if showsUserLocation { + switch showsUserLocationMode { + case .follow: mapView.showsUserLocation = true mapView.userTrackingMode = .follow - } else { + case .show: + mapView.showsUserLocation = true + mapView.userTrackingMode = .none + case .hide: mapView.showsUserLocation = false mapView.userTrackingMode = .none } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift index 3178bcb873..0c623230cd 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift @@ -80,8 +80,8 @@ struct LocationSharingViewState: BindableState { var showLoadingIndicator = false - /// True to indicate to show and follow current user location - var showsUserLocation = false + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user + var showsUserLocationMode: ShowUserLocationMode = .hide /// Used to hide live location sharing features var isLiveLocationSharingEnabled = false diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift index 39dbea5c81..76f7231e7c 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift @@ -40,7 +40,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie userAvatarData: avatarData, annotations: [], highlightedAnnotation: nil, - showsUserLocation: true, + showsUserLocationMode: .follow, isLiveLocationSharingEnabled: isLiveLocationSharingEnabled) super.init(initialViewState: viewState) @@ -73,7 +73,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie completion?(.share(latitude: pinLocation.latitude, longitude: pinLocation.longitude, coordinateType: .pin)) case .goToUserLocation: - state.showsUserLocation = true + state.showsUserLocationMode = .follow state.isPinDropSharing = false case .startLiveSharing: startLiveLocationSharing() @@ -81,7 +81,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie state.bindings.showingTimerSelector = false completion?(.shareLiveLocation(timeout: timeout.rawValue)) case .userDidPan: - state.showsUserLocation = false + state.showsUserLocationMode = .hide state.isPinDropSharing = true case .mapCreditsDidTap: state.bindings.showMapCreditsSheet.toggle() diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift index a2acb30f17..0ed0301cc2 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift @@ -92,7 +92,7 @@ struct LocationSharingView: View { annotations: context.viewState.annotations, highlightedAnnotation: context.viewState.highlightedAnnotation, userAvatarData: context.viewState.userAvatarData, - showsUserLocation: context.viewState.showsUserLocation, + showsUserLocationMode: context.viewState.showsUserLocationMode, userLocation: $context.userLocation, mapCenterCoordinate: $context.pinLocation, errorSubject: context.viewState.errorSubject, diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift index 0789b3edeb..8c1ece5693 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift @@ -43,7 +43,8 @@ struct StaticLocationViewingViewState: BindableState { /// Shared annotation to display existing location let sharedAnnotation: LocationAnnotation - var showsUserLocation = false + /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user + var showsUserLocationMode: ShowUserLocationMode = .hide var showLoadingIndicator = false diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift index d597771bca..f7b4dbfe46 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift @@ -98,7 +98,7 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static private func showsCurrentUserLocation() { if staticLocationSharingViewerService.requestAuthorizationIfNeeded() { - state.showsUserLocation = true + state.showsUserLocationMode = .follow } else { state.errorSubject.send(.invalidLocationAuthorization) } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift index 8350975ef3..7a2f30da40 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift @@ -81,9 +81,9 @@ class StaticLocationViewingViewModelTests: XCTestCase { func testToggleShowUserLocation() { let viewModel = buildViewModel() - XCTAssertFalse(viewModel.context.viewState.showsUserLocation) + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .hide) viewModel.context.send(viewAction: .showUserLocation) - XCTAssertTrue(viewModel.context.viewState.showsUserLocation) + XCTAssertEqual(viewModel.context.viewState.showsUserLocationMode, .follow) } private func buildViewModel() -> StaticLocationViewingViewModel { diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift index 30144268ea..8af60d429b 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift @@ -34,7 +34,7 @@ struct StaticLocationView: View { annotations: [viewModel.viewState.sharedAnnotation], highlightedAnnotation: viewModel.viewState.sharedAnnotation, userAvatarData: nil, - showsUserLocation: viewModel.viewState.showsUserLocation, + showsUserLocationMode: viewModel.viewState.showsUserLocationMode, userLocation: Binding.constant(nil), mapCenterCoordinate: Binding.constant(nil), errorSubject: viewModel.viewState.errorSubject) diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift index 9a25f8689d..5a7de9c922 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift @@ -41,12 +41,12 @@ final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordin let avatarData = showAvatar ? AvatarInput( mxContentUri: room.summary.avatar, matrixItemId: room.roomId, - displayName: room.summary.displayname + displayName: room.summary.displayName ) : nil let viewModel = RoomNotificationSettingsSwiftUIViewModel( roomNotificationService: roomNotificationService, avatarData: avatarData, - displayName: room.summary.displayname, + displayName: room.summary.displayName, roomEncrypted: room.summary.isEncrypted ) let avatarService: AvatarServiceProtocol = AvatarService(mediaManager: room.mxSession.mediaManager) diff --git a/RiotSwiftUI/Modules/Room/WaitingForMembers/View/RoomWaitingForMembers.swift b/RiotSwiftUI/Modules/Room/WaitingForMembers/View/RoomWaitingForMembers.swift new file mode 100644 index 0000000000..2b4827b819 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/WaitingForMembers/View/RoomWaitingForMembers.swift @@ -0,0 +1,50 @@ +// +// 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 SwiftUI + +struct RoomWaitingForMembers: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var body: some View { + ZStack { + HStack(alignment: .top) { + Image(uiImage: Asset.Images.membersListIcon.image) + VStack(alignment: .leading, spacing: 6) { + Text(VectorL10n.roomWaitingOtherParticipantsTitle(AppInfo.current.displayName)) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(VectorL10n.roomWaitingOtherParticipantsMessage(AppInfo.current.displayName)) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(9) + .background(theme.colors.system) + .cornerRadius(4) + } + } +} + +struct RoomWaitingForMembers_Previews: PreviewProvider { + static var previews: some View { + RoomWaitingForMembers() + .padding(16) + } +} diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift index 292ad2ee06..fca75dd384 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift @@ -27,7 +27,7 @@ extension MatrixListItemData { if parentSpaceIds.isEmpty { detailText = nil } else { - if let spaceName = spaceService.getSpace(withId: parentSpaceIds.first ?? "")?.summary?.displayname { + if let spaceName = spaceService.getSpace(withId: parentSpaceIds.first ?? "")?.summary?.displayName { let count = parentSpaceIds.count - 1 switch count { case 0: @@ -51,6 +51,6 @@ extension MatrixListItemData { } else { type = .room } - self.init(id: mxRoom.roomId, type: type, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayname, detailText: detailText) + self.init(id: mxRoom.roomId, type: type, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayName, detailText: detailText) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift index ad7e56d2a1..2d9a025c61 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift @@ -47,7 +47,7 @@ final class SpaceCreationCoordinator: Coordinator { init(parameters: SpaceCreationCoordinatorParameters) { let title: String let message: String - if let parentSpaceId = parameters.parentSpaceId, let parentSpaceName = parameters.session.spaceService.getSpace(withId: parentSpaceId)?.summary?.displayname { + if let parentSpaceId = parameters.parentSpaceId, let parentSpaceName = parameters.session.spaceService.getSpace(withId: parentSpaceId)?.summary?.displayName { title = VectorL10n.spacesSubspaceCreationVisibilityTitle message = VectorL10n.spacesSubspaceCreationVisibilityMessage(parentSpaceName) } else { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift index 4db35bf69d..3339055c91 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift @@ -77,7 +77,7 @@ class SpaceSelectorService: SpaceSelectorServiceProtocol { return nil } - return summary.displayname + return summary.displayName } // MARK: Public @@ -117,7 +117,7 @@ private extension SpaceSelectorListItemData { return SpaceSelectorListItemData(id: summary.roomId, avatar: summary.room.avatarData, - displayName: summary.displayname, + displayName: summary.displayName, notificationCount: notificationState?.groupMissedDiscussionsCount ?? 0, highlightedNotificationCount: notificationState?.groupMissedDiscussionsHighlightedCount ?? 0, hasSubItems: !space.childSpaces.isEmpty, diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift index 364aa98f8d..27f214bdd6 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift @@ -28,7 +28,7 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { private(set) var roomInitializationStatus: CurrentValueSubject var roomName: String? { - room.summary.displayname + room.summary.displayName } init(room: MXRoom) { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift index cd8b136493..dccb8b9f44 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift @@ -35,6 +35,6 @@ class TemplateRoomListService: TemplateRoomListServiceProtocol { private extension TemplateRoomListRoom { init(mxRoom: MXRoom) { - self.init(id: mxRoom.roomId, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayname) + self.init(id: mxRoom.roomId, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayName) } } diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index 22987f7b88..c07c158305 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -34,6 +34,7 @@ targets: platform: iOS dependencies: - target: DesignKit + - package: AnalyticsEvents - package: Mapbox - package: WysiwygComposer sources: diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index a25f394aef..85c77f05ca 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -35,6 +35,7 @@ targets: dependencies: - target: RiotSwiftUI + - package: AnalyticsEvents - package: WysiwygComposer settings: diff --git a/RiotSwiftUI/targetUnitTests.yml b/RiotSwiftUI/targetUnitTests.yml index 6362bf4472..ec16e078a3 100644 --- a/RiotSwiftUI/targetUnitTests.yml +++ b/RiotSwiftUI/targetUnitTests.yml @@ -35,6 +35,7 @@ targets: dependencies: - target: RiotSwiftUI + - package: AnalyticsEvents configFiles: Debug: Debug.xcconfig diff --git a/RiotTests/AnalyticsTests.swift b/RiotTests/AnalyticsTests.swift index 0fda6af36c..33ed283895 100644 --- a/RiotTests/AnalyticsTests.swift +++ b/RiotTests/AnalyticsTests.swift @@ -78,7 +78,7 @@ class AnalyticsTests: XCTestCase { XCTAssertNil(client.pendingUserProperties, "No user properties should have been set yet.") // When updating the user properties - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5)) // Then the properties should be cached XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -90,7 +90,7 @@ class AnalyticsTests: XCTestCase { func testMergingUserProperties() { // Given a client with a cached use case user properties let client = PostHogAnalyticsClient() - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil)) XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") @@ -98,7 +98,7 @@ class AnalyticsTests: XCTestCase { XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.") // When updating the number of spaces - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5)) // Then the new properties should be updated and the existing properties should remain unchanged XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -107,7 +107,7 @@ class AnalyticsTests: XCTestCase { XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.") // When updating the number of spaces - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: .Favourites)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: .Favourites, ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil)) // Then the new properties should be updated and the existing properties should remain unchanged XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -120,7 +120,7 @@ class AnalyticsTests: XCTestCase { func testSendingUserProperties() { // Given a client with user properties set let client = PostHogAnalyticsClient() - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil)) client.start() XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") @@ -136,7 +136,7 @@ class AnalyticsTests: XCTestCase { func testSendingUserPropertiesWithIdentify() { // Given a client with user properties set let client = PostHogAnalyticsClient() - client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, allChatsActiveFilter: nil)) + client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil)) client.start() XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") diff --git a/RiotTests/Experiments/CryptoSDKFeatureTests.swift b/RiotTests/Experiments/CryptoSDKFeatureTests.swift new file mode 100644 index 0000000000..ffcf045213 --- /dev/null +++ b/RiotTests/Experiments/CryptoSDKFeatureTests.swift @@ -0,0 +1,79 @@ +// +// 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 +import XCTest +@testable import Element + +class CryptoSDKFeatureTests: XCTestCase { + class RemoteFeatureClient: RemoteFeaturesClientProtocol { + var isEnabled = false + func isFeatureEnabled(_ feature: String) -> Bool { + isEnabled + } + } + + var remote: RemoteFeatureClient! + var feature: CryptoSDKFeature! + + override func setUp() { + RiotSettings.shared.enableCryptoSDK = false + remote = RemoteFeatureClient() + feature = CryptoSDKFeature(remoteFeature: remote) + } + + override func tearDown() { + RiotSettings.shared.enableCryptoSDK = false + } + + func test_disabledByDefault() { + XCTAssertFalse(feature.isEnabled) + } + + func test_enable() { + feature.enable() + XCTAssertTrue(feature.isEnabled) + } + + func test_enableIfAvailable_remainsEnabledWhenRemoteClientDisabled() { + feature.enable() + remote.isEnabled = false + + feature.enableIfAvailable(forUserId: "alice") + + XCTAssertTrue(feature.isEnabled) + } + + func test_enableIfAvailable_notEnabledIfRemoteFeatureDisabled() { + remote.isEnabled = false + feature.enableIfAvailable(forUserId: "alice") + XCTAssertFalse(feature.isEnabled) + } + + func test_canManuallyEnable() { + remote.isEnabled = false + XCTAssertTrue(feature.canManuallyEnable(forUserId: "alice")) + + remote.isEnabled = true + XCTAssertFalse(feature.canManuallyEnable(forUserId: "alice")) + } + + func test_reset() { + feature.enable() + feature.reset() + XCTAssertFalse(RiotSettings.shared.enableCryptoSDK) + } +} diff --git a/RiotTests/Experiments/ExperimentTests.swift b/RiotTests/Experiments/ExperimentTests.swift new file mode 100644 index 0000000000..ea0b8af2b9 --- /dev/null +++ b/RiotTests/Experiments/ExperimentTests.swift @@ -0,0 +1,67 @@ +// +// 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 +import XCTest +@testable import Element + +class ExperimentTests: XCTestCase { + + private func randomUserId() -> String { + return "user_" + UUID().uuidString.prefix(6) + } + + func test_singleVariant() { + let experiment = Experiment(name: "single", variants: 1) + for _ in 0 ..< 1000 { + let variant = experiment.variant(userId: randomUserId()) + XCTAssertEqual(variant, 0) + } + } + + func test_twoVariants() { + let experiment = Experiment(name: "two", variants: 2) + + var variants = Set() + for _ in 0 ..< 1000 { + let variant = experiment.variant(userId: randomUserId()) + variants.insert(variant) + } + + // We perform the test by collecting all assigned variants for 1000 users + // and ensuring we only encounter variants 0 and 1 + XCTAssertEqual(variants.count, 2) + XCTAssertTrue(variants.contains(0)) + XCTAssertTrue(variants.contains(1)) + XCTAssertFalse(variants.contains(2)) + } + + func test_manyVariants() { + let experiment = Experiment(name: "many", variants: 5) + + var variants = Set() + for _ in 0 ..< 10000 { + let variant = experiment.variant(userId: randomUserId()) + variants.insert(variant) + } + + // We perform the test by collecting all assigned variants for 10000 users + // and ensuring we only encounter variants between 0 and 4 + XCTAssertTrue(variants.count >= 2 && variants.count <= 5) + XCTAssertTrue(variants.isSubset(of: .init([0, 1, 2, 3, 4]))) + XCTAssertFalse(variants.contains(5)) + } +} diff --git a/RiotTests/Experiments/PhasedRolloutFeatureTests.swift b/RiotTests/Experiments/PhasedRolloutFeatureTests.swift new file mode 100644 index 0000000000..3c50078d36 --- /dev/null +++ b/RiotTests/Experiments/PhasedRolloutFeatureTests.swift @@ -0,0 +1,55 @@ +// +// 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 +import XCTest +@testable import Element + +class PhasedRolloutFeatureTests: XCTestCase { + + private func randomUserId() -> String { + return "user_" + UUID().uuidString.prefix(6) + } + + func test_allDisabled() { + let feature = PhasedRolloutFeature(name: "disabled", targetPercentage: 0) + for _ in 0 ..< 1000 { + let isEnabled = feature.isEnabled(userId: randomUserId()) + XCTAssertFalse(isEnabled) + } + } + + func test_allEnabled() { + let feature = PhasedRolloutFeature(name: "enabled", targetPercentage: 1) + for _ in 0 ..< 1000 { + let isEnabled = feature.isEnabled(userId: randomUserId()) + XCTAssertTrue(isEnabled) + } + } + + func test_someEnabled() { + let feature = PhasedRolloutFeature(name: "some", targetPercentage: 0.5) + var statuses = Set() + for _ in 0 ..< 1000 { + let isEnabled = feature.isEnabled(userId: randomUserId()) + statuses.insert(isEnabled) + } + + // We test by checking that we encountered both enabled and disabled cases + XCTAssertTrue(statuses.contains(true)) + XCTAssertTrue(statuses.contains(false)) + } +} diff --git a/RiotTests/target.yml b/RiotTests/target.yml index dd7736b6fd..37782a61e4 100644 --- a/RiotTests/target.yml +++ b/RiotTests/target.yml @@ -35,6 +35,7 @@ targets: dependencies: - target: Riot + - package: AnalyticsEvents configFiles: Debug: Debug.xcconfig @@ -60,8 +61,6 @@ targets: - path: . - path: ../Config/Configurable.swift - path: ../Config/BuildSettings.swift - - path: ../Config/AppConfiguration.swift - - path: ../Config/CommonConfiguration.swift - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Managers/AppInfo/AppInfo.swift - path: ../Riot/Managers/AppInfo/AppVersion.swift diff --git a/SiriIntents/ContactResolver/ContactResolver.m b/SiriIntents/ContactResolver/ContactResolver.m index 42d1212610..15fa233fac 100644 --- a/SiriIntents/ContactResolver/ContactResolver.m +++ b/SiriIntents/ContactResolver/ContactResolver.m @@ -122,7 +122,7 @@ - (void)resolveContacts:(nullable NSArray *)contacts INPersonHandle *personHandle = [[INPersonHandle alloc] initWithValue:user.userId type:INPersonHandleTypeUnknown]; // For rooms we try to use room display name - NSString *displayName = summary.displayname ? summary.displayname : user.displayname; + NSString *displayName = summary.displayName ? summary.displayName : user.displayname; INPerson *person = [[INPerson alloc] initWithPersonHandle:personHandle nameComponents:nil diff --git a/SiriIntents/target.yml b/SiriIntents/target.yml index 82f7a89da9..1894d1b9e4 100644 --- a/SiriIntents/target.yml +++ b/SiriIntents/target.yml @@ -33,6 +33,7 @@ targets: dependencies: - sdk: Intents.framework + - package: AnalyticsEvents - package: DeviceKit - package: DTCoreText @@ -45,6 +46,7 @@ targets: - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Categories/MXEvent.swift - path: ../Config/CommonConfiguration.swift + - path: ../Riot/Experiments/ - path: ../Config/BuildSettings.swift - path: ../Config/Configurable.swift - path: ../Riot/Managers/Settings/RiotSettings.swift diff --git a/project.yml b/project.yml index 8d5de91100..acc69ccdc7 100644 --- a/project.yml +++ b/project.yml @@ -40,6 +40,9 @@ include: - path: CommonKit/targetUnitTests.yml packages: + AnalyticsEvents: + url: https://github.com/matrix-org/matrix-analytics-events + exactVersion: 0.5.0 Mapbox: url: https://github.com/maplibre/maplibre-gl-native-distribution minVersion: 5.12.2 @@ -50,7 +53,7 @@ packages: maxVersion: 2.0.0 SwiftOGG: url: https://github.com/vector-im/swift-ogg - branch: main + branch: 0.0.1 WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift version: 1.1.1 @@ -59,4 +62,4 @@ packages: majorVersion: 4.7.0 DTCoreText: url: https://github.com/Cocoanetics/DTCoreText - version: 1.6.27 \ No newline at end of file + version: 1.6.27