From 4660f096f8d0e16eee1ac54b89aa1f11b829a673 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 12 Sep 2022 21:34:53 +0300 Subject: [PATCH] Fixes vector-im/element-x-ios/issues/117 - Event permalink timeline action * moved NSRegularExpression outside of the AttributedString builder into the MatrixEntityRegex * fixed eventId v3 regex * added permalink builders for users, room identifiers and aliases, and events * added timeline item permalink contextual menu actions and error alerts * added an app wide ServiceLocator and moved the top level userIndicatorPresenter to it. * added URL constructor that takes a StaticString and returns an non-optional * Include Unit and UI tests in the swiftlint search paths --- .swiftlint.yml | 2 + ElementX.xcodeproj/project.pbxproj | 14 +++ .../en.lproj/Untranslated.strings | 2 + ElementX/Sources/AppCoordinator.swift | 32 +++--- ElementX/Sources/BuildSettings.swift | 10 +- .../Generated/Strings+Untranslated.swift | 2 + ElementX/Sources/Other/Extensions/URL.swift | 27 +++++ .../HTMLParsing/AttributedStringBuilder.swift | 28 +---- .../Sources/Other/MatrixEntityRegex.swift | 52 ++++++++- ElementX/Sources/Other/PermalinkBuilder.swift | 102 ++++++++++++++++++ .../LoginScreen/LoginHomeserver.swift | 6 +- .../Screens/RoomScreen/RoomScreenModels.swift | 9 ++ .../RoomScreen/RoomScreenViewModel.swift | 18 +++- .../Screens/RoomScreen/View/RoomScreen.swift | 1 + .../View/TimelineItemContextMenu.swift | 8 +- .../Services/Authentication/OIDCService.swift | 3 +- .../Services/BugReport/BugReportService.swift | 29 ++--- .../Timeline/MockRoomTimelineController.swift | 2 + .../Timeline/RoomTimelineController.swift | 3 + .../RoomTimelineControllerProtocol.swift | 2 + UITests/SupportingFiles/target.yml | 1 + .../AttributedStringBuilderTests.swift | 40 +++---- UnitTests/Sources/BugReportServiceTests.swift | 22 ++-- UnitTests/Sources/PermalinkBuilderTests.swift | 101 +++++++++++++++++ UnitTests/SupportingFiles/target.yml | 1 + 25 files changed, 414 insertions(+), 103 deletions(-) create mode 100644 ElementX/Sources/Other/Extensions/URL.swift create mode 100644 ElementX/Sources/Other/PermalinkBuilder.swift create mode 100644 UnitTests/Sources/PermalinkBuilderTests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 8edf7ba67..1228f3593 100755 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -14,6 +14,8 @@ opt_in_rules: # paths to include during linting. `--path` is ignored if present. included: - ElementX + - UnitTests + - UITests - Tools/Scripts/Templates excluded: - IntegrationTests diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index be0ed4128..53c5777a6 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; 06E93B2E3B32740B40F47CC5 /* ElementNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF4B39D52CAE7D21D276ABEE /* ElementNavigationController.swift */; }; + 071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 07240B7159A3990C4C2E8FFC /* LoginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */; }; 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; }; 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; }; @@ -62,6 +63,7 @@ 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CF12478983A5EB390FB26 /* MessageComposer.swift */; }; 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */; }; 2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; + 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */; }; 28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */; }; 290FDB0FFDC2F1DDF660343E /* TestMeasurementParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */; }; 297CD0A27C87B0C50FF192EE /* RoomTimelineViewFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */; }; @@ -86,6 +88,7 @@ 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; }; 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; }; 3588F34D05B4D731A73214C6 /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED59F9EFF273BFA2055FFDF /* BugReportScreen.swift */; }; + 35C57543D245E82CBFE15DF0 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; }; 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; }; @@ -188,6 +191,7 @@ 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */; }; 7FED310F6AB7A70CBFB7C8A3 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C483956FA3D665E3842E319A /* SettingsScreen.swift */; }; 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; }; + 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; }; 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */; }; 83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */; }; 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */; }; @@ -416,6 +420,7 @@ 2112A6CFEA46E672D90EBF54 /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kab; path = kab.lproj/Localizable.strings; sourceTree = ""; }; 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineItemProtocol.swift; sourceTree = ""; }; 21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = ""; }; + 227AC5D71A4CE43512062243 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 22B384D54464FA39C6C7F6E7 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ca; path = ca.lproj/Localizable.stringsdict; sourceTree = ""; }; 233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = ""; }; 24A534A4619D8FEFB6439FCC /* SplashScreenPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenPageView.swift; sourceTree = ""; }; @@ -553,6 +558,7 @@ 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = ""; }; 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = ""; }; 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresentationContext.swift; sourceTree = ""; }; + 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkBuilderTests.swift; sourceTree = ""; }; 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenModels.swift; sourceTree = ""; }; 71D52BAA5BADB06E5E8C295D /* Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; @@ -775,6 +781,7 @@ F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = ""; }; F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = ""; }; + F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkBuilder.swift; sourceTree = ""; }; F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = ""; }; @@ -1060,6 +1067,7 @@ children = ( B6E89E530A8E92EC44301CA1 /* Bundle.swift */, 40B21E611DADDEF00307E7AC /* String.swift */, + 227AC5D71A4CE43512062243 /* URL.swift */, ); path = Extensions; sourceTree = ""; @@ -1268,6 +1276,7 @@ C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, A05707BF550D770168A406DB /* LoginViewModelTests.swift */, + 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */, EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */, @@ -1619,6 +1628,7 @@ 1027BB9A852F445B7623897F /* ElementSettings.swift */, 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, + F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */, BB3073CCD77D906B330BC1D6 /* Tests.swift */, 44BBB96FAA2F0D53C507396B /* Extensions */, 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */, @@ -2191,6 +2201,7 @@ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */, + 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */, 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */, @@ -2326,6 +2337,7 @@ 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */, CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */, + 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */, 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */, DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, BF35062D06888FA80BD139FF /* Presentable.swift in Sources */, @@ -2416,6 +2428,7 @@ 004561D297DC8B9786AE136F /* UITestScreenIdentifier.swift in Sources */, 03CB204C52F18E24A5C3D219 /* UITestsAppCoordinator.swift in Sources */, 17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */, + 071A017E415AD378F2961B11 /* URL.swift in Sources */, 8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */, 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */, 0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */, @@ -2461,6 +2474,7 @@ C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */, 0ED951768EC443A8728DE1D7 /* TimelineStyle.swift in Sources */, 75D98001C5AC38B6A5CA897C /* UITestScreenIdentifier.swift in Sources */, + 35C57543D245E82CBFE15DF0 /* URL.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index cd02c044c..0cb5196fb 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -11,6 +11,8 @@ "room_timeline_style_plain_long_description" = "Plain Timeline"; "room_timeline_style_bubbled_long_description" = "Bubbled Timeline"; +"room_timeline_permalink_creation_failure" = "Failed creating the permalink"; + // MARK: - Authentication "authentication_login_title" = "Welcome back!"; diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index 9fa891215..bdb3b62f4 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -18,6 +18,19 @@ import Combine import MatrixRustSDK import UIKit +struct ServiceLocator { + fileprivate static var serviceLocator: ServiceLocator? + static var shared: ServiceLocator { + guard let serviceLocator = serviceLocator else { + fatalError("The service locator should be setup at this point") + } + + return serviceLocator + } + + let userIndicatorPresenter: UserIndicatorTypePresenter +} + class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { private let window: UIWindow @@ -38,7 +51,6 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { private let screenshotDetector: ScreenshotDetector private let backgroundTaskService: BackgroundTaskServiceProtocol - private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? private var statusIndicator: UserIndicator? @@ -46,13 +58,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { init() { stateMachine = AppCoordinatorStateMachine() - - do { - bugReportService = try BugReportService(withBaseUrlString: BuildSettings.bugReportServiceBaseUrlString, - sentryEndpoint: BuildSettings.bugReportSentryEndpoint) - } catch { - fatalError(error.localizedDescription) - } + + bugReportService = BugReportService(withBaseURL: BuildSettings.bugReportServiceBaseURL, sentryURL: BuildSettings.bugReportSentryURL) splashViewController = SplashViewController() mainNavigationController = ElementNavigationController(rootViewController: splashViewController) @@ -64,7 +71,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { memberDetailProviderManager = MemberDetailProviderManager() - indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: mainNavigationController) + ServiceLocator.serviceLocator = ServiceLocator(userIndicatorPresenter: UserIndicatorTypePresenter(presentingViewController: mainNavigationController)) guard let bundleIdentifier = Bundle.main.bundleIdentifier else { fatalError("Should have a valid bundle identifier at this point") @@ -256,6 +263,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { attributedStringBuilder: AttributedStringBuilder()) let timelineController = RoomTimelineController(userId: userId, + roomId: roomIdentifier, timelineProvider: RoomTimelineProvider(roomProxy: roomProxy), timelineItemFactory: timelineItemFactory, mediaProvider: userSession.mediaProvider, @@ -409,7 +417,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { // MARK: Toasts and loading indicators private func showLoadingIndicator() { - loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true)) + loadingIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true)) } private func hideLoadingIndicator() { @@ -417,10 +425,10 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { } private func showLoginErrorToast() { - statusIndicator = indicatorPresenter.present(.error(label: "Failed logging in")) + statusIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.error(label: "Failed logging in")) } private func showLogoutErrorToast() { - statusIndicator = indicatorPresenter.present(.error(label: "Failed logging out")) + statusIndicator = ServiceLocator.shared.userIndicatorPresenter.present(.error(label: "Failed logging out")) } } diff --git a/ElementX/Sources/BuildSettings.swift b/ElementX/Sources/BuildSettings.swift index 020363d9f..7a190abd8 100644 --- a/ElementX/Sources/BuildSettings.swift +++ b/ElementX/Sources/BuildSettings.swift @@ -23,14 +23,14 @@ final class BuildSettings { // MARK: - Bug report - static let bugReportServiceBaseUrlString = "https://riot.im/bugreports" - static let bugReportSentryEndpoint = "https://f39ac49e97714316965b777d9f3d6cd8@sentry.tools.element.io/44" + static let bugReportServiceBaseURL = URL(staticString: "https://riot.im/bugreports") + static let bugReportSentryURL = URL(staticString: "https://f39ac49e97714316965b777d9f3d6cd8@sentry.tools.element.io/44") // Use the name allocated by the bug report server static let bugReportApplicationId = "riot-ios" static let bugReportUISIId = "element-auto-uisi" static let bugReportGHLabels = ["Element-X"] - + // MARK: - Analytics #if DEBUG @@ -57,4 +57,8 @@ final class BuildSettings { // MARK: - Room screen static let defaultRoomTimelineStyle: TimelineStyle = .bubbles + + // MARK: - Other + + static var permalinkBaseURL = URL(staticString: "https://matrix.to") } diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index e15e875ab..4f0c44813 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -22,6 +22,8 @@ extension ElementL10n { public static let authenticationServerInfoMatrixDescription = ElementL10n.tr("Untranslated", "authentication_server_info_matrix_description") /// Choose your server to store your data public static let authenticationServerInfoTitle = ElementL10n.tr("Untranslated", "authentication_server_info_title") + /// Failed creating the permalink + public static let roomTimelinePermalinkCreationFailure = ElementL10n.tr("Untranslated", "room_timeline_permalink_creation_failure") /// Bubbled Timeline public static let roomTimelineStyleBubbledLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_bubbled_long_description") /// Plain Timeline diff --git a/ElementX/Sources/Other/Extensions/URL.swift b/ElementX/Sources/Other/Extensions/URL.swift new file mode 100644 index 000000000..7632c8f44 --- /dev/null +++ b/ElementX/Sources/Other/Extensions/URL.swift @@ -0,0 +1,27 @@ +// +// Copyright 2022 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 URL { + init(staticString: StaticString) { + guard let url = URL(string: "\(staticString)") else { + fatalError("The static string used to create this URL is invalid") + } + + self = url + } +} diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index 9cae20a47..446002056 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -22,24 +22,6 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { private let temporaryCodeBlockMarkingColor = UIColor.cyan private let linkColor = UIColor.blue - private let userIdDetector: NSRegularExpression - private let roomIdDetector: NSRegularExpression - private let eventIdDetector: NSRegularExpression - private let roomAliasDetector: NSRegularExpression - private let linkDetector: NSDataDetector - - init() { - do { - userIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) - roomIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.roomId.rawValue, options: .caseInsensitive) - eventIdDetector = try NSRegularExpression(pattern: MatrixEntityRegex.eventId.rawValue, options: .caseInsensitive) - roomAliasDetector = try NSRegularExpression(pattern: MatrixEntityRegex.roomAlias.rawValue, options: .caseInsensitive) - linkDetector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - } catch { - fatalError() - } - } - func fromPlain(_ string: String?) async -> AttributedString? { await Task.detached { fromPlain(string) @@ -181,11 +163,11 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { let string = attributedString.string let range = NSRange(location: 0, length: attributedString.string.count) - var matches = userIdDetector.matches(in: string, options: [], range: range) - matches.append(contentsOf: roomIdDetector.matches(in: string, options: [], range: range)) - matches.append(contentsOf: eventIdDetector.matches(in: string, options: [], range: range)) - matches.append(contentsOf: roomAliasDetector.matches(in: string, options: [], range: range)) - matches.append(contentsOf: linkDetector.matches(in: string, options: [], range: range)) + var matches = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: [], range: range) + matches.append(contentsOf: MatrixEntityRegex.roomIdentifierRegex.matches(in: string, options: [], range: range)) + matches.append(contentsOf: MatrixEntityRegex.eventIdentifierRegex.matches(in: string, options: [], range: range)) + matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string, options: [], range: range)) + matches.append(contentsOf: MatrixEntityRegex.linkRegex.matches(in: string, options: [], range: range)) guard matches.count > 0 else { return diff --git a/ElementX/Sources/Other/MatrixEntityRegex.swift b/ElementX/Sources/Other/MatrixEntityRegex.swift index e9d40dba7..46ac9a6c8 100644 --- a/ElementX/Sources/Other/MatrixEntityRegex.swift +++ b/ElementX/Sources/Other/MatrixEntityRegex.swift @@ -16,6 +16,7 @@ import Foundation +// https://spec.matrix.org/latest/appendices/#identifier-grammar enum MatrixEntityRegex: String { case homeserver case userId @@ -34,7 +35,56 @@ enum MatrixEntityRegex: String { case .roomId: return "![A-Z0-9]+:" + MatrixEntityRegex.homeserver.rawValue case .eventId: - return "\\$[A-Z0-9]+:" + MatrixEntityRegex.homeserver.rawValue + return "\\$[A-Z0-9\\/+]+" } } + + // swiftlint:disable force_try + static var homeserverRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.homeserver.rawValue, options: .caseInsensitive) + static var userIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) + static var roomAliasRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.roomAlias.rawValue, options: .caseInsensitive) + static var roomIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.roomId.rawValue, options: .caseInsensitive) + static var eventIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.eventId.rawValue, options: .caseInsensitive) + static var linkRegex = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + // swiftlint:enable force_try + + static func isMatrixHomeserver(_ homeserver: String) -> Bool { + guard let match = userIdentifierRegex.firstMatch(in: homeserver, range: .init(location: 0, length: homeserver.count)) else { + return false + } + + return match.range.length == homeserver.count + } + + static func isMatrixUserIdentifier(_ identifier: String) -> Bool { + guard let match = userIdentifierRegex.firstMatch(in: identifier, range: .init(location: 0, length: identifier.count)) else { + return false + } + + return match.range.length == identifier.count + } + + static func isMatrixRoomAlias(_ alias: String) -> Bool { + guard let match = roomAliasRegex.firstMatch(in: alias, range: .init(location: 0, length: alias.count)) else { + return false + } + + return match.range.length == alias.count + } + + static func isMatrixRoomIdentifier(_ identifier: String) -> Bool { + guard let match = roomIdentifierRegex.firstMatch(in: identifier, range: .init(location: 0, length: identifier.count)) else { + return false + } + + return match.range.length == identifier.count + } + + static func isMatrixEventIdentifier(_ identifier: String) -> Bool { + guard let match = eventIdentifierRegex.firstMatch(in: identifier, range: .init(location: 0, length: identifier.count)) else { + return false + } + + return match.range.length == identifier.count + } } diff --git a/ElementX/Sources/Other/PermalinkBuilder.swift b/ElementX/Sources/Other/PermalinkBuilder.swift new file mode 100644 index 000000000..9691e944c --- /dev/null +++ b/ElementX/Sources/Other/PermalinkBuilder.swift @@ -0,0 +1,102 @@ +// +// Copyright 2022 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 + +enum PermalinkBuilderError: Error { + case invalidUserIdentifier + case invalidRoomIdentifier + case invalidRoomAlias + case invalidEventIdentifier + case failedConstructingURL + case failedAddingPercentEncoding +} + +enum PermalinkBuilder { + static var uriComponentCharacterSet: CharacterSet = { + var charset = CharacterSet.alphanumerics + charset.insert(charactersIn: "-_.!~*'()") + return charset + }() + + static func permalinkTo(userIdentifier: String) throws -> URL { + guard MatrixEntityRegex.isMatrixUserIdentifier(userIdentifier) else { + throw PermalinkBuilderError.invalidUserIdentifier + } + + let urlString = "\(BuildSettings.permalinkBaseURL)/#/\(userIdentifier)" + + guard let url = URL(string: urlString) else { + throw PermalinkBuilderError.failedConstructingURL + } + + return url + } + + static func permalinkTo(roomIdentifier: String) throws -> URL { + guard MatrixEntityRegex.isMatrixRoomIdentifier(roomIdentifier) else { + throw PermalinkBuilderError.invalidRoomIdentifier + } + + return try permalinkTo(roomIdentifierOrAlias: roomIdentifier) + } + + static func permalinkTo(roomAlias: String) throws -> URL { + guard MatrixEntityRegex.isMatrixRoomAlias(roomAlias) else { + throw PermalinkBuilderError.invalidRoomAlias + } + + return try permalinkTo(roomIdentifierOrAlias: roomAlias) + } + + static func permalinkTo(eventIdentifier: String, roomIdentifier: String) throws -> URL { + guard MatrixEntityRegex.isMatrixEventIdentifier(eventIdentifier) else { + throw PermalinkBuilderError.invalidEventIdentifier + } + guard MatrixEntityRegex.isMatrixRoomIdentifier(roomIdentifier) else { + throw PermalinkBuilderError.invalidRoomIdentifier + } + + guard let roomId = roomIdentifier.addingPercentEncoding(withAllowedCharacters: uriComponentCharacterSet), + let eventId = eventIdentifier.addingPercentEncoding(withAllowedCharacters: uriComponentCharacterSet) else { + throw PermalinkBuilderError.failedAddingPercentEncoding + } + + let urlString = "\(BuildSettings.permalinkBaseURL)/#/\(roomId)/\(eventId)" + + guard let url = URL(string: urlString) else { + throw PermalinkBuilderError.failedConstructingURL + } + + return url + } + + // MARK: - Private + + private static func permalinkTo(roomIdentifierOrAlias: String) throws -> URL { + guard let identifier = roomIdentifierOrAlias.addingPercentEncoding(withAllowedCharacters: uriComponentCharacterSet) else { + throw PermalinkBuilderError.failedAddingPercentEncoding + } + + let urlString = "\(BuildSettings.permalinkBaseURL)/#/\(identifier)" + + guard let url = URL(string: urlString) else { + throw PermalinkBuilderError.failedConstructingURL + } + + return url + } +} diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift index c332639de..786576593 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift @@ -64,8 +64,10 @@ extension LoginHomeserver { /// A mock homeserver that supports only supports authentication via a single SSO provider. static var mockOIDC: LoginHomeserver { - // swiftlint:disable:next force_unwrapping - let issuerURL = URL(string: "https://auth.company.com")! + guard let issuerURL = URL(string: "https://auth.company.com") else { + fatalError("This shoud never fail parsing") + } + return LoginHomeserver(address: "company.com", loginMode: .oidc(issuerURL)) } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 4d14cb890..a3f5fec8f 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -22,6 +22,7 @@ enum RoomScreenViewModelAction { } enum TimelineItemContextMenuAction: Hashable { case copy case quote + case copyPermalink } enum RoomScreenViewAction { @@ -49,4 +50,12 @@ struct RoomScreenViewState: BindableState { struct RoomScreenViewStateBindings { var composerText: String + + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + +enum RoomScreenErrorType: Hashable { + /// A specific error message shown in an alert. + case alert(String) } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 37b8a72bd..e2c062040 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -116,7 +116,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol return [] } - return [.copy, .quote] + return [.copy, .quote, .copyPermalink] } private func processContentMenuAction(_ action: TimelineItemContextMenuAction, itemId: String) { @@ -130,6 +130,22 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol UIPasteboard.general.string = item.text case .quote: state.bindings.composerText = "> \(item.text)" + case .copyPermalink: + do { + let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: item.id, roomIdentifier: timelineController.roomId) + UIPasteboard.general.url = permalink + } catch { + displayError(.alert(ElementL10n.roomTimelinePermalinkCreationFailure)) + } + } + } + + private func displayError(_ type: RoomScreenErrorType) { + switch type { + case .alert(let message): + state.bindings.alertInfo = AlertInfo(id: type, + title: ElementL10n.dialogTitleError, + message: message) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index f39794bb8..7ed2818bb 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -33,6 +33,7 @@ struct RoomScreen: View { RoomHeaderView(context: context) } } + .alert(item: $context.alertInfo) { $0.alert } } private func sendMessage() { diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift index 6eb851f2b..f4f35287c 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift @@ -25,11 +25,15 @@ public struct TimelineItemContextMenu: View { ForEach(contextMenuActions, id: \.self) { item in switch item { case .copy: - Button("Copy") { + Button(ElementL10n.actionCopy) { callback(item) } case .quote: - Button("Quote") { + Button(ElementL10n.actionQuote) { + callback(item) + } + case .copyPermalink: + Button(ElementL10n.permalink) { callback(item) } } diff --git a/ElementX/Sources/Services/Authentication/OIDCService.swift b/ElementX/Sources/Services/Authentication/OIDCService.swift index 58ef2352a..1177c297f 100644 --- a/ElementX/Sources/Services/Authentication/OIDCService.swift +++ b/ElementX/Sources/Services/Authentication/OIDCService.swift @@ -34,8 +34,7 @@ class OIDCService { private var metadata: OIDServiceConfiguration? /// Redirect URI for the request. Must match the `client_uri` in reverse DNS format. - private let redirectURI = URL(string: "io.element:/callback")! - // swiftlint:disable:previous force_unwrapping + private var redirectURI = URL(staticString: "io.element:/callback") /// Maintains a strong ref to the authorization session that's in progress. private var session: OIDExternalUserAgentSession? diff --git a/ElementX/Sources/Services/BugReport/BugReportService.swift b/ElementX/Sources/Services/BugReport/BugReportService.swift index ab630d038..eb634a183 100644 --- a/ElementX/Sources/Services/BugReport/BugReportService.swift +++ b/ElementX/Sources/Services/BugReport/BugReportService.swift @@ -20,40 +20,29 @@ import MatrixRustSDK import Sentry import UIKit -enum BugReportServiceError: Error { - case invalidBaseUrlString - case invalidSentryEndpoint -} - class BugReportService: BugReportServiceProtocol { private let baseURL: URL - private let sentryEndpoint: String + private let sentryURL: URL private let applicationId: String private let session: URLSession private var lastCrashEventId: String? - - init(withBaseUrlString baseUrlString: String, - sentryEndpoint: String, + + init(withBaseURL baseURL: URL, + sentryURL: URL, applicationId: String = BuildSettings.bugReportApplicationId, - session: URLSession = .shared) throws { - guard let url = URL(string: baseUrlString) else { - throw BugReportServiceError.invalidBaseUrlString - } - guard !sentryEndpoint.isEmpty else { - throw BugReportServiceError.invalidSentryEndpoint - } - baseURL = url - self.sentryEndpoint = sentryEndpoint + session: URLSession = .shared) { + self.baseURL = baseURL + self.sentryURL = sentryURL self.applicationId = applicationId self.session = session - + // enable SentrySDK SentrySDK.start { options in #if DEBUG options.enabled = false #endif - options.dsn = sentryEndpoint + options.dsn = sentryURL.absoluteString // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. // We recommend adjusting this value in production. diff --git a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift index e15ee7b50..3acd03ca6 100644 --- a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift @@ -18,6 +18,8 @@ import Combine import Foundation class MockRoomTimelineController: RoomTimelineControllerProtocol { + let roomId = "MockRoomIdentifier" + let callbacks = PassthroughSubject() var timelineItems: [RoomTimelineItemProtocol] = [SeparatorRoomTimelineItem(id: UUID().uuidString, diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index 40335bdca..12e2ef8f9 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -32,16 +32,19 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } + let roomId: String let callbacks = PassthroughSubject() private(set) var timelineItems = [RoomTimelineItemProtocol]() init(userId: String, + roomId: String, timelineProvider: RoomTimelineProviderProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol, memberDetailProvider: MemberDetailProviderProtocol) { self.userId = userId + self.roomId = roomId self.timelineProvider = timelineProvider self.timelineItemFactory = timelineItemFactory self.mediaProvider = mediaProvider diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift index 05ace11d1..3b93965ea 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift @@ -28,6 +28,8 @@ enum RoomTimelineControllerError: Error { @MainActor protocol RoomTimelineControllerProtocol { + var roomId: String { get } + var timelineItems: [RoomTimelineItemProtocol] { get } var callbacks: PassthroughSubject { get } diff --git a/UITests/SupportingFiles/target.yml b/UITests/SupportingFiles/target.yml index 47d9d4d40..6ff385f04 100644 --- a/UITests/SupportingFiles/target.yml +++ b/UITests/SupportingFiles/target.yml @@ -87,3 +87,4 @@ targets: - path: ../../ElementX/Sources/Generated/InfoPlist.swift - path: ../../ElementX/Resources - path: ../../ElementX/Sources/Other/Extensions/Bundle.swift + - path: ../../ElementX/Sources/Other/Extensions/URL.swift diff --git a/UnitTests/Sources/AttributedStringBuilderTests.swift b/UnitTests/Sources/AttributedStringBuilderTests.swift index 18668b041..b20de1a47 100644 --- a/UnitTests/Sources/AttributedStringBuilderTests.swift +++ b/UnitTests/Sources/AttributedStringBuilderTests.swift @@ -181,7 +181,7 @@ class AttributedStringBuilderTests: XCTestCase { } func testEventIdLink() async { - let eventId = "$eventidentifier:matrix.org" + let eventId = "$eventidentifier" let string = "The event is \(eventId)." checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromHTML(string), expected: eventId) checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromPlain(string), expected: eventId) @@ -254,10 +254,8 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString)?.count, 1) - for run in attributedString.runs { - if run.elementX.blockquote != nil { - return - } + for run in attributedString.runs where run.elementX.blockquote ?? false { + return } XCTFail("Couldn't find blockquote") @@ -280,10 +278,8 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString)?.count, 3) - for run in attributedString.runs { - if run.elementX.blockquote != nil { - return - } + for run in attributedString.runs where run.elementX.blockquote ?? false { + return } XCTFail("Couldn't find blockquote") @@ -310,10 +306,8 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(coalescedComponents.first?.attributedString.runs.count, 3, "Link not present in the component") var foundBlockquoteAndLink = false - for run in attributedString.runs { - if run.elementX.blockquote != nil, run.link != nil { - foundBlockquoteAndLink = true - } + for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { + foundBlockquoteAndLink = true } XCTAssertNotNil(foundBlockquoteAndLink, "Couldn't find blockquote or link") @@ -336,10 +330,8 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString)?.count, 1) var numberOfBlockquotes = 0 - for run in attributedString.runs { - if run.elementX.blockquote != nil, run.link != nil { - numberOfBlockquotes += 1 - } + for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { + numberOfBlockquotes += 1 } XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes") @@ -365,10 +357,8 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString)?.count, 6) var numberOfBlockquotes = 0 - for run in attributedString.runs { - if run.elementX.blockquote != nil, run.link != nil { - numberOfBlockquotes += 1 - } + for run in attributedString.runs where run.elementX.blockquote ?? false && run.link != nil { + numberOfBlockquotes += 1 } XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes") @@ -384,11 +374,9 @@ class AttributedStringBuilderTests: XCTestCase { XCTAssertEqual(attributedString.runs.count, 3) - for run in attributedString.runs { - if run.link != nil { - XCTAssertEqual(run.link?.path, expected) - return - } + for run in attributedString.runs where run.link != nil { + XCTAssertEqual(run.link?.path, expected) + return } XCTFail("Couldn't find expected value.") diff --git a/UnitTests/Sources/BugReportServiceTests.swift b/UnitTests/Sources/BugReportServiceTests.swift index a66562e19..a3ed892b4 100644 --- a/UnitTests/Sources/BugReportServiceTests.swift +++ b/UnitTests/Sources/BugReportServiceTests.swift @@ -33,21 +33,21 @@ class BugReportServiceTests: XCTestCase { files: []) XCTAssertFalse(result.reportUrl.isEmpty) } - + func testInitialStateWithRealService() throws { - let service = try BugReportService(withBaseUrlString: "https://www.example.com", - sentryEndpoint: "mock_sentry_dsn", - applicationId: "mock_app_id", - session: .mock) + let service = BugReportService(withBaseURL: URL(staticString: "https://www.example.com"), + sentryURL: URL(staticString: "https://1234@sentry.com/1234"), + applicationId: "mock_app_id", + session: .mock) XCTAssertFalse(service.crashedLastRun) } - + @MainActor func testSubmitBugReportWithRealService() async throws { - let service = try BugReportService(withBaseUrlString: "https://www.example.com", - sentryEndpoint: "mock_sentry_dsn", - applicationId: "mock_app_id", - session: .mock) - + let service = BugReportService(withBaseURL: URL(staticString: "https://www.example.com"), + sentryURL: URL(staticString: "https://1234@sentry.com/1234"), + applicationId: "mock_app_id", + session: .mock) + let result = try await service.submitBugReport(text: "i cannot send message", includeLogs: true, includeCrashLog: true, diff --git a/UnitTests/Sources/PermalinkBuilderTests.swift b/UnitTests/Sources/PermalinkBuilderTests.swift new file mode 100644 index 000000000..ac97a86df --- /dev/null +++ b/UnitTests/Sources/PermalinkBuilderTests.swift @@ -0,0 +1,101 @@ +// +// Copyright 2022 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. +// + +@testable import ElementX +import XCTest + +class PermalinkBuilderTests: XCTestCase { + func testUserIdentifierPermalink() { + let userId = "@abcdefghijklmnopqrstuvwxyz1234567890._-=/:matrix.org" + + do { + let permalink = try PermalinkBuilder.permalinkTo(userIdentifier: userId) + XCTAssertEqual(permalink, URL(string: "\(BuildSettings.permalinkBaseURL)/#/\(userId)")) + } catch { + XCTFail("User identifier must be valid: \(error)") + } + } + + func testInvalidUserIdentifier() { + do { + _ = try PermalinkBuilder.permalinkTo(userIdentifier: "This1sN0tV4lid!@#$%^&*()") + XCTFail("A permalink should not be created.") + } catch { + XCTAssertEqual(error as? PermalinkBuilderError, PermalinkBuilderError.invalidUserIdentifier) + } + } + + func testRoomIdentifierPermalink() throws { + let roomId = "!abcdefghijklmnopqrstuvwxyz1234567890:matrix.org" + + do { + let permalink = try PermalinkBuilder.permalinkTo(roomIdentifier: roomId) + XCTAssertEqual(permalink, URL(string: "\(BuildSettings.permalinkBaseURL)/#/!abcdefghijklmnopqrstuvwxyz1234567890%3Amatrix.org")) + } catch { + XCTFail("Room identifier must be valid: \(error)") + } + } + + func testInvalidRoomIdentifier() { + do { + _ = try PermalinkBuilder.permalinkTo(roomIdentifier: "This1sN0tV4lid!@#$%^&*()") + XCTFail("A permalink should not be created.") + } catch { + XCTAssertEqual(error as? PermalinkBuilderError, PermalinkBuilderError.invalidRoomIdentifier) + } + } + + func testRoomAliasPermalink() throws { + let roomAlias = "#abcdefghijklmnopqrstuvwxyz-_.1234567890:matrix.org" + + do { + let permalink = try PermalinkBuilder.permalinkTo(roomAlias: roomAlias) + XCTAssertEqual(permalink, URL(string: "\(BuildSettings.permalinkBaseURL)/#/%23abcdefghijklmnopqrstuvwxyz-_.1234567890%3Amatrix.org")) + } catch { + XCTFail("Room alias must be valid: \(error)") + } + } + + func testInvalidRoomAlias() { + do { + _ = try PermalinkBuilder.permalinkTo(roomAlias: "This1sN0tV4lid!@#$%^&*()") + XCTFail("A permalink should not be created.") + } catch { + XCTAssertEqual(error as? PermalinkBuilderError, PermalinkBuilderError.invalidRoomAlias) + } + } + + func testEventPermalink() throws { + let eventId = "$abcdefghijklmnopqrstuvwxyz1234567890" + let roomId = "!abcdefghijklmnopqrstuvwxyz1234567890:matrix.org" + + do { + let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: eventId, roomIdentifier: roomId) + XCTAssertEqual(permalink, URL(string: "\(BuildSettings.permalinkBaseURL)/#/!abcdefghijklmnopqrstuvwxyz1234567890%3Amatrix.org/%24abcdefghijklmnopqrstuvwxyz1234567890")) + } catch { + XCTFail("Room and event identifiers must be valid: \(error)") + } + } + + func testInvalidEventIdentifier() { + do { + _ = try PermalinkBuilder.permalinkTo(eventIdentifier: "This1sN0tV4lid!@#$%^&*()", roomIdentifier: "") + XCTFail("A permalink should not be created.") + } catch { + XCTAssertEqual(error as? PermalinkBuilderError, PermalinkBuilderError.invalidEventIdentifier) + } + } +} diff --git a/UnitTests/SupportingFiles/target.yml b/UnitTests/SupportingFiles/target.yml index b6d9a875c..ed95492d0 100644 --- a/UnitTests/SupportingFiles/target.yml +++ b/UnitTests/SupportingFiles/target.yml @@ -49,3 +49,4 @@ targets: - path: ../SupportingFiles - path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit - path: ../Resources + \ No newline at end of file