Move string formatting to Tools.

Revert contacts tracking from MatrixKit.
Final tweaks before PR.
This commit is contained in:
Doug
2021-12-07 15:08:48 +00:00
parent 299cc8bc74
commit ecd4bbd63b
23 changed files with 142 additions and 151 deletions

3
.gitignore vendored
View File

@@ -29,9 +29,6 @@ vendor/
#
Pods/
# Never commit auto-generated secrets even if pods are checked in
Pods/CocoaPodsKeys/
## Ignore project files as we generate them with xcodegen (https://github.com/yonaskolb/XcodeGen)
*.xcodeproj
*.xcworkspace

View File

@@ -166,10 +166,12 @@ final class BuildSettings: NSObject {
// MARK: - Analytics
#warning("Testing environment.")
// Optional host for PostHog analytics. Set to nil to disable analytics.
/// Host to use for PostHog analytics. Set to nil to disable analytics.
static let analyticsHost: String? = "https://posthog-poc.lab.element.dev"
// Public key for submitting analytics. Set to nil to disable analytics.
/// Public key for submitting analytics. Set to nil to disable analytics.
static let analyticsKey: String? = "rs-pJjsYJTuAkXJfhaMmPUNBhWliDyTKLOOxike6ck8"
/// The URL to open with more information about analytics terms.
static let analyticsTermsURL = URL(string: "https://element.io/cookie-policy")!
// MARK: - Bug report

View File

@@ -299,4 +299,4 @@ DEPENDENCIES
xcode-install
BUNDLED WITH
2.2.31
2.2.28

View File

@@ -69,8 +69,8 @@ abstract_target 'RiotPods' do
# PostHog for analytics
pod 'PostHog', '~> 1.4.4'
# pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift'
pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec'
pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift'
# pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec'
# Remove warnings from "bad" pods
pod 'OLMKit', :inhibit_warnings => true
@@ -129,6 +129,7 @@ abstract_target 'RiotPods' do
end
post_install do |installer|
installer.pods_project.targets.each do |target|

View File

@@ -57,29 +57,16 @@ PODS:
- LoggerAPI (1.9.200):
- Logging (~> 1.1)
- Logging (1.4.0)
- MatrixKit (0.16.10):
- Down (~> 0.11.0)
- DTCoreText (~> 1.6.25)
- HPGrowingTextView (~> 1.1)
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixKit/Core (= 0.16.10)
- MatrixSDK (= 0.20.10)
- MatrixKit/Core (0.16.10):
- Down (~> 0.11.0)
- DTCoreText (~> 1.6.25)
- HPGrowingTextView (~> 1.1)
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixSDK (= 0.20.10)
- MatrixSDK (0.20.10):
- MatrixSDK/Core (= 0.20.10)
- MatrixSDK/Core (0.20.10):
- MatrixSDK (0.20.13):
- MatrixSDK/Core (= 0.20.13)
- MatrixSDK/Core (0.20.13):
- AFNetworking (~> 4.0.0)
- GZIP (~> 1.3.0)
- libbase58 (~> 0.1.4)
- OLMKit (~> 3.2.5)
- Realm (= 10.16.0)
- SwiftyBeaver (= 1.9.5)
- MatrixSDK/JingleCallStack (0.20.10):
- MatrixSDK/JingleCallStack (0.20.13):
- JitsiMeetSDK (= 3.10.2)
- MatrixSDK/Core
- OLMKit (3.2.5):
@@ -115,19 +102,22 @@ PODS:
- ZXingObjC/All (3.6.5)
DEPENDENCIES:
- AnalyticsEvents (from `../matrix-analytics-events/AnalyticsEvents.podspec`)
- AnalyticsEvents (from `https://github.com/matrix-org/matrix-analytics-events.git`, branch `release/swift`)
- DGCollectionViewLeftAlignFlowLayout (~> 1.0.4)
- Down (~> 0.11.0)
- DSWaveformImage (~> 6.1.1)
- DTCoreText (~> 1.6.25)
- ffmpeg-kit-ios-audio (~> 4.5)
- FLEX (~> 4.5.0)
- FlowCommoniOS (~> 1.12.0)
- GBDeviceInfo (~> 6.6.0)
- HPGrowingTextView (~> 1.1)
- Introspect (~> 0.1)
- KeychainAccess (~> 4.2.2)
- KTCenterFlowLayout (~> 1.3.1)
- MatrixKit (= 0.16.10)
- MatrixSDK
- MatrixSDK/JingleCallStack
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixSDK (= 0.20.13)
- MatrixSDK/JingleCallStack (= 0.20.13)
- OLMKit
- PostHog (~> 1.4.4)
- ReadMoreTextView (~> 3.0.1)
@@ -167,7 +157,6 @@ SPEC REPOS:
- libPhoneNumber-iOS
- LoggerAPI
- Logging
- MatrixKit
- MatrixSDK
- OLMKit
- PostHog
@@ -186,11 +175,17 @@ SPEC REPOS:
EXTERNAL SOURCES:
AnalyticsEvents:
:path: "../matrix-analytics-events/AnalyticsEvents.podspec"
:branch: release/swift
:git: https://github.com/matrix-org/matrix-analytics-events.git
CHECKOUT OPTIONS:
AnalyticsEvents:
:commit: aac06956d45cb86ea2bbd7a21b20b14ba8899fcf
:git: https://github.com/matrix-org/matrix-analytics-events.git
SPEC CHECKSUMS:
AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce
AnalyticsEvents: 27b0d074e839d2d354d12ae679930e373cba5f45
AnalyticsEvents: 333bf47d67dc628fadd29ce887b7ac93d8bd6e05
BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24
BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc
BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3
@@ -214,8 +209,7 @@ SPEC CHECKSUMS:
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
Logging: beeb016c9c80cf77042d62e83495816847ef108b
MatrixKit: c3f0bb056ceeb015e2f1688543ac4dbcf88bef2f
MatrixSDK: 0e2ed8fc6f004cac4b4ab46f038a86fe49ce4007
MatrixSDK: 945f082654830d7ae3a6e1e068b6dc22b2eae932
OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d
@@ -231,6 +225,6 @@ SPEC CHECKSUMS:
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 9819df47b1ebbcea325ba9f24baa92878f3d0efe
PODFILE CHECKSUM: f4ad67860350a28588e177245d1d0aff0fdcf186
COCOAPODS: 1.11.2

View File

@@ -165,13 +165,6 @@ extension Analytics {
func trackIdentityServerAccepted(_ accepted: Bool) {
// Do we still want to track this?
}
/// Track whether the user granted or rejected access to the device contacts.
/// **Note** This method isn't currently implemented.
/// - Parameter granted: Whether access was granted.
func trackContactsAccessGranted(_ granted: Bool) {
// Do we still want to track this?
}
}
// MARK: - MXAnalyticsDelegate
@@ -217,4 +210,9 @@ extension Analytics: MXAnalyticsDelegate {
let event = AnalyticsEvent.JoinedRoom(isDM: isDM, roomSize: roomSize)
capture(event: event)
}
/// **Note** This method isn't currently implemented.
func trackContactsAccessGranted(_ granted: Bool) {
// Do we still want to track this?
}
}

View File

@@ -1,44 +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
protocol DictionaryConvertible: Encodable {
var dictionary: [String: Any] { get }
}
extension DictionaryConvertible {
var dictionary: [String: Any] {
let mirror = Mirror(reflecting: self)
let dict: [String: Any] = Dictionary(uniqueKeysWithValues: mirror.children.compactMap { (label: String?, value: Any) in
guard let label = label else { return nil }
// Handle standard types such as String/Int/Bool
if let value = value as? NSCoding {
return (label, value)
}
// AnalyticsEvent enums
if let value = value as? CustomStringConvertible {
return (label, value.description)
}
return nil
})
return dict
}
}

View File

@@ -59,5 +59,4 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
postHog?.screen(event.screenName.rawValue, properties: event.properties)
}
}

View File

@@ -101,14 +101,12 @@ final class RiotSettings: NSObject {
// MARK: Other
/// Whether the user has both seen the Matomo analytics prompt and declined it.
/// This is used to prevent users who previously opted out from being asked again.
/// Whether the user was previously shown the Matomo analytics prompt.
var hasSeenAnalyticsPrompt: Bool {
RiotSettings.defaults.object(forKey: UserDefaultsKeys.enableAnalytics) != nil
}
/// Whether the user has both seen the Matomo analytics prompt and declined it.
/// This is used to prevent users who previously opted out from being asked again.
var hasDeclinedMatomoAnalytics: Bool {
RiotSettings.defaults.object(forKey: UserDefaultsKeys.matomoAnalytics) != nil && !RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics)
}
@@ -119,11 +117,13 @@ final class RiotSettings: NSObject {
RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics)
}
/// Indicates if the device has already called identify for this session to PostHog.
/// `true` when the user has opted in to send analytics.
@UserDefault(key: UserDefaultsKeys.enableAnalytics, defaultValue: false, storage: defaults)
var enableAnalytics
/// Indicates if the device has already called identify for this session to PostHog.
/// This is separate to `enableAnalytics` as logging out will leave analytics
/// enabled but reset identification.
@UserDefault(key: "isIdentifiedForAnalytics", defaultValue: false, storage: defaults)
var isIdentifiedForAnalytics

View File

@@ -648,7 +648,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
MXLogDebug(@"[AppDelegate] afterAppUnlockedByPin");
// Check if there is crash log to send
if (Analytics.shared.isRunning)
if (RiotSettings.shared.enableAnalytics)
{
[self checkExceptionToReport];
}

View File

@@ -883,7 +883,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
// Request address book access
[[CNContactStore new] requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
[Analytics.shared trackContactsAccessGranted:granted];
[MXSDKOptions.sharedInstance.analyticsDelegate trackContactsAccessGranted:granted];
dispatch_async(dispatch_get_main_queue(), ^{

View File

@@ -59,4 +59,14 @@
*/
+ (NSAttributedString *)setTextColorAlpha:(CGFloat)alpha inAttributedString:(NSAttributedString*)attributedString;
/** Builds an attributed string from a string containing html.
@param htmlString The html string to use.
@param allowedTags The html tags that should be allowed.
Note: It is recommended to include "p" and "body" tags in
`allowedTags` as these are often added when parsing.
*/
+ (NSAttributedString * _Nonnull)attributedStringFromHTML:(NSString * _Nonnull)htmlString
withAllowedTags:(NSArray<NSString *> * _Nonnull)allowedTags;
@end

View File

@@ -138,4 +138,41 @@
return string;
}
+ (NSAttributedString *)attributedStringFromHTML:(NSString *)htmlString withAllowedTags:(NSArray<NSString *> *)allowedTags
{
UIFont *font = [UIFont systemFontOfSize:[UIFont systemFontSize]];
// Do some sanitisation before finalizing the string
DTHTMLAttributedStringBuilderWillFlushCallback sanitizeCallback = ^(DTHTMLElement *element) {
[element sanitizeWith:allowedTags bodyFont:font imageHandler:nil];
};
NSDictionary *options = @{
DTUseiOS6Attributes: @(YES), // Enable it to be able to display the attributed string in a UITextView
DTDefaultFontFamily: font.familyName,
DTDefaultFontName: font.fontName,
DTDefaultFontSize: @(font.pointSize),
DTDefaultLinkDecoration: @(NO),
DTWillFlushBlockCallBack: sanitizeCallback
};
// Do not use the default HTML renderer of NSAttributedString because this method
// runs on the UI thread which we want to avoid because renderHTMLString is called
// most of the time from a background thread.
// Use DTCoreText HTML renderer instead.
// Using DTCoreText, which renders static string, helps to avoid code injection attacks
// that could happen with the default HTML renderer of NSAttributedString which is a
// webview.
NSAttributedString *string = [[NSAttributedString alloc] initWithHTMLData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:NULL];
// Apply additional treatments
string = [MXKTools removeDTCoreTextArtifacts:string];
if (!string) {
return [[NSAttributedString alloc] initWithString:htmlString];
}
return string;
}
@end

35
Riot/Utils/Tools.swift Normal file
View File

@@ -0,0 +1,35 @@
//
// 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
extension Tools {
/// Builds an attributed string by replacing a `%@` placeholder with the supplied link text and URL.
/// - Parameters:
/// - string: The string to be formatted.
/// - link: The link text to be inserted.
/// - url: The URL to be linked to.
/// - Returns: An attributed string.
static func format(_ string: String, with link: String, using url: URL) -> NSAttributedString {
let baseString = NSMutableAttributedString(string: string)
let attributedLink = NSAttributedString(string: link, attributes: [.link: url])
let linkRange = (baseString.string as NSString).range(of: "%@")
baseString.replaceCharacters(in: linkRange, with: attributedLink)
return baseString
}
}

View File

@@ -121,7 +121,3 @@ extension AnalyticsPromptType: Identifiable {
}
}
}
extension NSAttributedString.Key {
static let analyticsPromptTermsTextLink = NSAttributedString.Key("TermsTextLink")
}

View File

@@ -29,6 +29,8 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType {
// MARK: - Properties
// MARK: Private
let termsURL: URL
// MARK: Public
@@ -37,7 +39,8 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType {
// MARK: - Setup
/// Initialize a view model with the specified prompt type and app display name.
init(promptType: AnalyticsPromptType, strings: AnalyticsPromptStringsProtocol) {
init(promptType: AnalyticsPromptType, strings: AnalyticsPromptStringsProtocol, termsURL: URL) {
self.termsURL = termsURL
super.init(initialViewState: AnalyticsPromptViewState(promptType: promptType, strings: strings))
}
@@ -70,7 +73,6 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType {
/// Open the service terms link.
private func openTermsURL() {
guard let url = URL(string: "https://element.io/cookie-policy") else { return }
UIApplication.shared.open(url)
UIApplication.shared.open(termsURL)
}
}

View File

@@ -61,7 +61,7 @@ final class AnalyticsPromptCoordinator: Coordinator {
promptType = .newUser(termsString: strings.termsNewUser)
}
let viewModel = AnalyticsPromptViewModel(promptType: promptType, strings: strings)
let viewModel = AnalyticsPromptViewModel(promptType: promptType, strings: strings, termsURL: BuildSettings.analyticsTermsURL)
let view = AnalyticsPrompt(viewModel: viewModel.context)
_analyticsPromptViewModel = viewModel

View File

@@ -20,52 +20,14 @@ import DTCoreText
struct AnalyticsPromptStrings: AnalyticsPromptStringsProtocol {
let appDisplayName = AppInfo.current.displayName
let point1: NSAttributedString
let point2: NSAttributedString
let point1 = Tools.attributedString(fromHTML: VectorL10n.analyticsPromptPoint1, withAllowedTags: ["b", "p"])
let point2 = Tools.attributedString(fromHTML: VectorL10n.analyticsPromptPoint2, withAllowedTags: ["b", "p"])
let termsNewUser: NSAttributedString
let termsUpgrade: NSAttributedString
init() {
self.point1 = Self.parse(VectorL10n.analyticsPromptPoint1)
self.point2 = Self.parse(VectorL10n.analyticsPromptPoint2)
self.termsNewUser = Self.attach(VectorL10n.analyticsPromptTermsLinkNewUser,
to: VectorL10n.analyticsPromptTermsNewUser("%@"))
self.termsUpgrade = Self.attach(VectorL10n.analyticsPromptTermsLinkUpgrade,
to: VectorL10n.analyticsPromptTermsUpgrade("%@"))
}
static func parse(_ htmlString: String) -> NSAttributedString {
// Do some sanitisation before finalizing the string
// let sanitizeCallback: DTHTMLAttributedStringBuilderWillFlushCallback = { element in
// element?.sanitize(with: ["b"], bodyFont: .systemFont(ofSize: UIFont.systemFontSize), imageHandler: nil)
// print("Hello")
// }
let options: [String: Any] = [
DTUseiOS6Attributes: true, // Enable it to be able to display the attributed string in a UITextView
DTDefaultLinkDecoration: false,
// DTWillFlushBlockCallBack: sanitizeCallback
]
guard let attributedString = NSAttributedString(htmlData: htmlString.data(using: .utf8),
options: options,
documentAttributes: nil) else {
return NSAttributedString(string: htmlString)
}
return MXKTools.removeDTCoreTextArtifacts(attributedString)
}
static func attach(_ link: String, to terms: String) -> NSAttributedString {
let baseString = NSMutableAttributedString(string: terms)
let linkRange = (baseString.string as NSString).range(of: "%@")
let formattedLink = NSAttributedString(string: VectorL10n.analyticsPromptTermsLinkNewUser,
attributes: [.analyticsPromptTermsTextLink: true])
baseString.replaceCharacters(in: linkRange, with: formattedLink)
return baseString
}
let termsNewUser = Tools.format(VectorL10n.analyticsPromptTermsNewUser("%@"),
with: VectorL10n.analyticsPromptTermsLinkNewUser,
using: BuildSettings.analyticsTermsURL)
let termsUpgrade = Tools.format(VectorL10n.analyticsPromptTermsUpgrade("%@"),
with: VectorL10n.analyticsPromptTermsLinkUpgrade,
using: BuildSettings.analyticsTermsURL)
}

View File

@@ -44,7 +44,8 @@ enum MockAnalyticsPromptScreenState: MockScreenState, CaseIterable {
promptType = analyticsPromptType
}
let viewModel = AnalyticsPromptViewModel(promptType: promptType,
strings: MockAnalyticsPromptStrings())
strings: MockAnalyticsPromptStrings(),
termsURL: URL(string: "https://element.io/cookie-policy")!)
return (
[promptType, viewModel],

View File

@@ -40,12 +40,12 @@ struct MockAnalyticsPromptStrings: AnalyticsPromptStringsProtocol {
self.point2 = point2
let termsNewUser = NSMutableAttributedString(string: "You can read all our terms ")
termsNewUser.append(NSAttributedString(string: "here", attributes: [.analyticsPromptTermsTextLink: true]))
termsNewUser.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!]))
termsNewUser.append(NSAttributedString(string: "."))
self.termsNewUser = termsNewUser
let termsUpgrade = NSMutableAttributedString(string: "Read all our terms ")
termsUpgrade.append(NSAttributedString(string: "here", attributes: [.analyticsPromptTermsTextLink: true]))
termsUpgrade.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!]))
termsUpgrade.append(NSAttributedString(string: ". Is that OK?"))
self.termsUpgrade = termsUpgrade
}

View File

@@ -43,7 +43,7 @@ struct AnalyticsPromptTermsText: View {
let string = attributedString.string as NSString
attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in
let isLink = attributes.keys.contains(.analyticsPromptTermsTextLink)
let isLink = attributes.keys.contains(.link)
components.append(StringComponent(string: string.substring(with: range), isLink: isLink))
}

1
changelog.d/5035.change Normal file
View File

@@ -0,0 +1 @@
Analytics: Replace Matomo with PostHog.