diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index d802b3557..ac49fde69 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -29,7 +29,6 @@ 09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; }; 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; - 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; }; 0BEFE400B4802FE8C9DB39B3 /* FilePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62BDF0FF4F59AF6EA858B70B /* FilePreviewViewModel.swift */; }; 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */; }; 0C38C3E771B472E27295339D /* SessionVerificationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */; }; @@ -208,7 +207,6 @@ 6CA81428F0970785CDCC5E86 /* UserNotificationToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */; }; 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 6E6E0AAF6C44C0B117EBBE5A /* SlidingSyncViewProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F3B445BD6EF1C751806B22 /* SlidingSyncViewProxy.swift */; }; - 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */; }; 6F2AB43A1EFAD8A97AF41A15 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; }; 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */; }; 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; }; @@ -303,7 +301,6 @@ 9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */; }; 9D2E03DB175A6AB14589076D /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA4E1BEB4E9BC2467006E12B /* AppAuth */; }; 9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; }; - 9DAF683E0CD7D70C8862EC98 /* TimelineItemReactionsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD61385D39FD330AFB928E6A /* TimelineItemReactionsMenuView.swift */; }; 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; 9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */; }; 9F41FF9C53F7A6EAEA6259C9 /* InviteFriendsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7AB0A148FCCAC28681C190 /* InviteFriendsCoordinator.swift */; }; @@ -383,7 +380,6 @@ C94A6048C654B01163AE1BF1 /* VideoPlayerViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5282B7A2DCD076AD2CF27F46 /* VideoPlayerViewModelProtocol.swift */; }; CA45758F08DF42D41D8A4B29 /* FilePreviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */; }; CB137BFB3E083C33E398A6CB /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; - CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */; }; CB498F4E27AA0545DCEF0F6F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; }; CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; CBF64DE774298D773DBD5354 /* VideoPlayerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB634B42CFE667112369D57 /* VideoPlayerScreen.swift */; }; @@ -447,7 +443,6 @@ F06CE9132855E81EBB6DDC32 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; }; F0F82C3C848C865C3098AA52 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; }; F257F964493A9CD02A6F720C /* OnboardingPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */; }; - F3B941FE3BBAA3320F18E73F /* EmojiPickerSearchFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C615E29435BC651DD2E95B67 /* EmojiPickerSearchFieldView.swift */; }; F425C3F85BFF28C9AC593F52 /* MockNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96561CC53F7C1E24D4C292E4 /* MockNotificationManager.swift */; }; F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; }; F61AFA8BF2E739FBC30472F5 /* NotificationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD5FEE195446A9E458DDDAF /* NotificationServiceProxyProtocol.swift */; }; @@ -561,7 +556,6 @@ 1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 124D85E85505B6B81845235F /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fy; path = fy.lproj/Localizable.stringsdict; sourceTree = ""; }; - 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizer.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetails.swift; sourceTree = ""; }; @@ -675,7 +669,6 @@ 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; - 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmark.swift; sourceTree = ""; }; 4A57A4AFA6A068668AFBD070 /* UIActivityViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityViewControllerWrapper.swift; sourceTree = ""; }; 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettings.swift; sourceTree = ""; }; 4B40B7F6FCCE2D8C242492D9 /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = ""; }; @@ -716,7 +709,6 @@ 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = ""; }; 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = ""; }; - 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizerTests.swift; sourceTree = ""; }; 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageProtocol.swift; sourceTree = ""; }; 616197D81103330BF2ADD559 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Localizable.strings; sourceTree = ""; }; 624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; @@ -866,7 +858,6 @@ ACA11F7F50A4A3887A18CA5A /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewProvider.swift; sourceTree = ""; }; AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; - AD61385D39FD330AFB928E6A /* TimelineItemReactionsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReactionsMenuView.swift; sourceTree = ""; }; AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportCoordinator.swift; sourceTree = ""; }; ADB3A7BCE745626EC61EF3C3 /* FilePreviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewCoordinator.swift; sourceTree = ""; }; ADCB8A232D3A8FB3E16A7303 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; @@ -912,7 +903,6 @@ C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelProtocol.swift; sourceTree = ""; }; C483956FA3D665E3842E319A /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; - C615E29435BC651DD2E95B67 /* EmojiPickerSearchFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerSearchFieldView.swift; sourceTree = ""; }; C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; }; C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = ""; }; C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -1269,7 +1259,6 @@ children = ( AB785716B9212C093704E767 /* EmojiPickerHeaderView.swift */, 00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */, - C615E29435BC651DD2E95B67 /* EmojiPickerSearchFieldView.swift */, ); path = View; sourceTree = ""; @@ -1677,7 +1666,6 @@ F3648F2FADEF2672D6A0D489 /* FileCacheTests.swift */, DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */, 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */, - 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */, FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */, C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, @@ -2139,9 +2127,7 @@ isa = PBXGroup; children = ( E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */, - 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */, E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */, - 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */, 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */, @@ -2192,7 +2178,6 @@ isa = PBXGroup; children = ( D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */, - AD61385D39FD330AFB928E6A /* TimelineItemReactionsMenuView.swift */, 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */, ); path = Supplementary; @@ -2837,7 +2822,6 @@ 7E7DF1867F98B0D10A6C0A63 /* FileCacheTests.swift in Sources */, CA45758F08DF42D41D8A4B29 /* FilePreviewViewModelTests.swift in Sources */, F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */, - 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */, A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */, EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, @@ -2903,7 +2887,6 @@ D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */, E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */, 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, - CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */, 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */, B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */, A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */, @@ -2938,7 +2921,6 @@ 748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */, 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */, 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */, - F3B941FE3BBAA3320F18E73F /* EmojiPickerSearchFieldView.swift in Sources */, FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */, 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */, 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */, @@ -2963,7 +2945,6 @@ DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */, 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */, 03D684A3AE85A23B3DA3B43F /* Image.swift in Sources */, - 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */, BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */, DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */, D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */, @@ -3127,7 +3108,6 @@ 01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */, F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */, 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */, - 9DAF683E0CD7D70C8862EC98 /* TimelineItemReactionsMenuView.swift in Sources */, 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */, ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */, 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index bf627bb40..af378dc5a 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -13,6 +13,7 @@ "room_timeline_style_bubbled_long_description" = "Bubbled Timeline"; "room_timeline_permalink_creation_failure" = "Failed creating the permalink"; +"room_timeline_backpagination_failure" = "Failed loading messages"; "room_timeline_replying_to" = "Replying to %@"; "room_timeline_editing" = "Editing"; diff --git a/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift b/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift index 7eb83d531..e5cb52f71 100644 --- a/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift +++ b/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift @@ -377,6 +377,8 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS } } + var presentationDetents: Set = [] + // The currently presented sheet coordinator // Sheets will be presented through the NavigationSplitCoordinator if provided var sheetCoordinator: (any CoordinatorProtocol)? { @@ -489,7 +491,8 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS // MARK: - CoordinatorProtocol func toPresentable() -> AnyView { - AnyView(NavigationStackCoordinatorView(navigationStackCoordinator: self)) + AnyView(NavigationStackCoordinatorView(navigationStackCoordinator: self) + .presentationDetents(presentationDetents)) } // MARK: - CustomStringConvertible diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index 3fd686bfe..0f2e65d48 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -30,6 +30,8 @@ extension ElementL10n { public static let roomDetailsAboutSectionTitle = ElementL10n.tr("Untranslated", "room_details_about_section_title") /// Info public static let roomDetailsTitle = ElementL10n.tr("Untranslated", "room_details_title") + /// Failed loading messages + public static let roomTimelineBackpaginationFailure = ElementL10n.tr("Untranslated", "room_timeline_backpagination_failure") /// Editing public static let roomTimelineEditing = ElementL10n.tr("Untranslated", "room_timeline_editing") /// Failed creating the permalink diff --git a/ElementX/Sources/Other/Benchmark.swift b/ElementX/Sources/Other/Benchmark.swift deleted file mode 100644 index cbcdeac43..000000000 --- a/ElementX/Sources/Other/Benchmark.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// 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 - -struct Benchmark { - private static var trackingIdentifiers = [String: CFAbsoluteTime]() - - public static var trackingEnabled = false - - static func startTrackingForIdentifier(_ identifier: String, message: String? = nil) { - guard trackingEnabled else { - return - } - - let startTime = CFAbsoluteTimeGetCurrent() - trackingIdentifiers[identifier] = startTime - - if let message { - MXLog.verbose("⏰ \(message).") - } - } - - static func logElapsedDurationForIdentifier(_ identifier: String, message: String? = nil) { - guard trackingEnabled else { - return - } - - guard let start = trackingIdentifiers[identifier] else { - assertionFailure("⏰ Invalid tracking identifier") - return - } - - let elapsedTime = CFAbsoluteTimeGetCurrent() - start - if let message { - MXLog.verbose("⏰ \(message). Elapsed time: \(elapsedTime.round(to: 4)) seconds.") - } else { - MXLog.verbose("⏰ Elapsed time: \(elapsedTime.round(to: 4)) seconds.") - } - } - - static func endTrackingForIdentifier(_ identifier: String, message: String? = nil) { - guard trackingEnabled else { - return - } - - logElapsedDurationForIdentifier(identifier, message: message) - trackingIdentifiers[identifier] = nil - } -} - -private extension Double { - func round(to places: Int) -> Double { - let divisor = pow(10.0, Double(places)) - return (self * divisor).rounded() / divisor - } -} diff --git a/ElementX/Sources/Other/ImageAnonymizer.swift b/ElementX/Sources/Other/ImageAnonymizer.swift deleted file mode 100644 index b8afe836a..000000000 --- a/ElementX/Sources/Other/ImageAnonymizer.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// 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 -import UIKit -import Vision - -enum ImageAnonymizerError: Error { - case noCgImageBased -} - -enum ImageAnonymizer { - private static var allowedTextItems: [String] = [ - "#", - "@", - "%", - "&", - "+", - "-", - "_", - "\"", - "?", - "*" - ] - - static func anonymizedImage(from image: UIImage, - confidenceLevel: Float = 0.5, - fillColor: UIColor = .red) async throws -> UIImage { - guard let cgImage = image.cgImage else { - throw ImageAnonymizerError.noCgImageBased - } - - // create a handler with cgImage - let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) - var observations: [VNDetectedObjectObservation] = [] - - // create a text request - let textRequest = VNRecognizeTextRequest { request, error in - guard let results = request.results as? [VNRecognizedTextObservation], - error == nil else { - return - } - observations.append(contentsOf: results) - } - textRequest.recognitionLevel = .accurate - textRequest.revision = VNRecognizeTextRequestRevision2 - - // create a face request - let faceRequest = VNDetectFaceRectanglesRequest { request, error in - guard let results = request.results as? [VNFaceObservation], - error == nil else { - return - } - observations.append(contentsOf: results) - } - // revision3 doesn't work! - faceRequest.revision = VNDetectFaceRectanglesRequestRevision2 - - #if targetEnvironment(simulator) - // Avoid `Could not create inference context` errors on Apple Silicon - // https://www.caseyliss.com/2022/6/20/feedback-is-broken-stop-trying-to-make-radar-happen - faceRequest.usesCPUOnly = true - #endif - - // perform requests - try handler.perform([ - textRequest, - faceRequest - ]) - - return render(image: image, - confidenceLevel: confidenceLevel, - fillColor: fillColor, - observations: observations) - } - - private static func render(image: UIImage, - confidenceLevel: Float, - fillColor: UIColor, - observations: [VNDetectedObjectObservation]) -> UIImage { - let size = image.size - let result = UIGraphicsImageRenderer(size: size).image { rendererContext in - // first draw self - image.draw(in: CGRect(origin: .zero, size: size)) - // set fill color - fillColor.setFill() - for observation in observations { - guard observation.confidence >= confidenceLevel else { - // ensure observation's confidence level - continue - } - if let textObservation = observation as? VNRecognizedTextObservation, - let text = textObservation.topCandidates(1).first?.string { - if Double(text) != nil || Self.allowedTextItems.contains(text) { - continue - } - } - let box = observation.boundingBox - // boc is normalized (and in starts from the lower left corner) - // convert it to a rect in the image - let rect = CGRect(x: box.minX * size.width, - y: size.height - box.maxY * size.height, - width: box.width * size.width, - height: box.height * size.height) - rendererContext.fill(rect) - } - } - return result - } -} diff --git a/ElementX/Sources/Other/NetworkMonitor.swift b/ElementX/Sources/Other/NetworkMonitor.swift index 85f53c02b..3f5c18283 100644 --- a/ElementX/Sources/Other/NetworkMonitor.swift +++ b/ElementX/Sources/Other/NetworkMonitor.swift @@ -34,7 +34,7 @@ class NetworkMonitor { init() { queue = DispatchQueue(label: "io.element.elementx.networkmonitor") pathMonitor = NWPathMonitor() - reachabilityPublisher = CurrentValueSubject(pathMonitor.currentPath.status == .satisfied) + reachabilityPublisher = CurrentValueSubject(true) pathMonitor.pathUpdateHandler = { [weak self] path in DispatchQueue.main.async { diff --git a/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift b/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift index ae76cf640..260b70db7 100644 --- a/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift +++ b/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift @@ -53,23 +53,11 @@ class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol { private func submitBugReport() async { callback?(.submitStarted) do { - var files: [URL] = [] - if let screenshot = state.screenshot { - let anonymized = try await ImageAnonymizer.anonymizedImage(from: screenshot) - let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("screenshot").appendingPathExtension("png") - // remove old screenshot if exists - if FileManager.default.fileExists(atPath: tmpUrl.path) { - try FileManager.default.removeItem(at: tmpUrl) - } - try anonymized.dataForPNGRepresentation().write(to: tmpUrl) - files.append(tmpUrl) - } - let result = try await bugReportService.submitBugReport(text: context.reportText, includeLogs: context.sendingLogsEnabled, includeCrashLog: true, githubLabels: ServiceLocator.shared.settings.bugReportGHLabels, - files: files) + files: []) MXLog.info("SubmitBugReport succeeded, result: \(result.reportUrl)") callback?(.submitFinished) } catch { diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift index 9437bd37d..e985d4f0d 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift @@ -23,6 +23,7 @@ struct EmojiPickerScreenCoordinatorParameters { enum EmojiPickerScreenCoordinatorAction { case emojiSelected(emoji: String, itemId: String) + case dismiss } final class EmojiPickerScreenCoordinator: CoordinatorProtocol { @@ -44,12 +45,13 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol { switch action { case let .emojiSelected(emoji: emoji): self.callback?(.emojiSelected(emoji: emoji, itemId: self.parameters.itemId)) + case .dismiss: + self.callback?(.dismiss) } } } func toPresentable() -> AnyView { - AnyView(EmojiPickerScreen(context: viewModel.context) - .presentationDetents([.medium, .large])) + AnyView(EmojiPickerScreen(context: viewModel.context)) } } diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift index 12b267b54..a4069eb5f 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift @@ -18,6 +18,7 @@ import Foundation enum EmojiPickerScreenViewModelAction { case emojiSelected(emoji: String) + case dismiss } struct EmojiPickerScreenViewState: BindableState { @@ -27,6 +28,7 @@ struct EmojiPickerScreenViewState: BindableState { enum EmojiPickerScreenViewAction { case search(searchString: String) case emojiTapped(emoji: EmojiPickerEmojiViewData) + case dismiss } struct EmojiPickerEmojiCategoryViewData: Identifiable { diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift index 43f4d0d4e..8e0e3ded6 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift @@ -39,6 +39,8 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr state.categories = convert(emojiCategories: categories) case let .emojiTapped(emoji: emoji): callback?(.emojiSelected(emoji: emoji.value)) + case .dismiss: + callback?(.dismiss) } } diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift index e12f10ad3..ebc6ea423 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift @@ -21,39 +21,49 @@ struct EmojiPickerScreen: View { @State var searchString = "" var body: some View { - VStack { - Text(ElementL10n.reactions) - .padding(.top, 20) - EmojiPickerSearchFieldView(searchString: $searchString) - .padding(.horizontal, 10) - ScrollView { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 45))], spacing: 3) { - ForEach(context.viewState.categories) { category in - Section(header: EmojiPickerHeaderView(title: category.name) - .padding(.horizontal, 13) - .padding(.top, 10)) { - ForEach(category.emojis) { emoji in - Text(emoji.value) - .frame(width: 45, height: 45) - .onTapGesture { - context.send(viewAction: .emojiTapped(emoji: emoji)) - } - } + ScrollView { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 45))], spacing: 3) { + ForEach(context.viewState.categories) { category in + Section(header: EmojiPickerHeaderView(title: category.name) + .padding(.horizontal, 13) + .padding(.top, 10)) { + ForEach(category.emojis) { emoji in + Text(emoji.value) + .frame(width: 45, height: 45) + .onTapGesture { + context.send(viewAction: .emojiTapped(emoji: emoji)) + } } - } + } } } } + .navigationTitle(ElementL10n.reactions) + .navigationBarTitleDisplayMode(.inline) + .toolbar { toolbar } + .searchable(text: $searchString) .onChange(of: searchString) { _ in context.send(viewAction: .search(searchString: searchString)) } } + + @ToolbarContentBuilder + var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + Button { context.send(viewAction: .dismiss) } label: { + Text(ElementL10n.actionCancel) + } + .accessibilityIdentifier("dismissButton") + } + } } // MARK: - Previews struct EmojiPickerScreen_Previews: PreviewProvider { static var previews: some View { - EmojiPickerScreen(context: EmojiPickerScreenViewModel(emojiProvider: EmojiProvider()).context) + NavigationStack { + EmojiPickerScreen(context: EmojiPickerScreenViewModel(emojiProvider: EmojiProvider()).context) + } } } diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerSearchFieldView.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerSearchFieldView.swift deleted file mode 100644 index 6cb7edc4a..000000000 --- a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerSearchFieldView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// 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 SwiftUI - -struct EmojiPickerSearchFieldView: View { - @Binding var searchString: String - @FocusState private var isSearchFocused: Bool - - var body: some View { - HStack { - Image(systemName: "magnifyingglass") - TextField(ElementL10n.search, text: $searchString) - .focused($isSearchFocused) - if isSearchFocused { - Spacer() - Button { - searchString = "" - isSearchFocused = false - } label: { - Text(ElementL10n.actionCancel) - } - } - } - } -} - -struct EmojiPickerSearchFieldView_Previews: PreviewProvider { - static var previews: some View { - EmojiPickerSearchFieldView(searchString: .constant("")) - } -} diff --git a/ElementX/Sources/Screens/Other/SplashScreenCoordinator.swift b/ElementX/Sources/Screens/Other/SplashScreenCoordinator.swift index 8def0d789..922ae819b 100644 --- a/ElementX/Sources/Screens/Other/SplashScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Other/SplashScreenCoordinator.swift @@ -19,7 +19,10 @@ import SwiftUI struct SplashScreenCoordinator: CoordinatorProtocol { func toPresentable() -> AnyView { AnyView( - Image(asset: Asset.Images.appLogo) + ZStack { + Image(asset: Asset.Images.appLogo) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) ) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 866dd9300..3fff580cd 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -107,6 +107,9 @@ final class RoomScreenCoordinator: CoordinatorProtocol { let timelineController = parameters?.timelineController else { fatalError() } + + let emojiPickerNavigationStackCoordinator = NavigationStackCoordinator() + let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider, itemId: itemId) let coordinator = EmojiPickerScreenCoordinator(parameters: params) @@ -118,10 +121,15 @@ final class RoomScreenCoordinator: CoordinatorProtocol { Task { await timelineController.sendReaction(emoji, for: itemId) } + case .dismiss: + self?.navigationStackCoordinator.setSheetCoordinator(nil) } } - navigationStackCoordinator.setSheetCoordinator(coordinator) + emojiPickerNavigationStackCoordinator.setRootCoordinator(coordinator) + emojiPickerNavigationStackCoordinator.presentationDetents = [.medium, .large] + + navigationStackCoordinator.setSheetCoordinator(emojiPickerNavigationStackCoordinator) } private func displayRoomDetails() { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 01c067a7c..457176912 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -33,7 +33,6 @@ enum RoomScreenComposerMode: Equatable { enum RoomScreenViewAction { case headerTapped case displayEmojiPicker(itemId: String) - case emojiTapped(emoji: String, itemId: String) case paginateBackwards case itemAppeared(id: String) case itemDisappeared(id: String) @@ -43,7 +42,6 @@ enum RoomScreenViewAction { case sendReaction(key: String, eventID: String) case cancelReply case cancelEdit - case displayReactionsMenuForItemId(itemId: String) } struct RoomScreenViewState: BindableState { @@ -54,7 +52,6 @@ struct RoomScreenViewState: BindableState { var isBackPaginating = false var showLoading = false var bindings: RoomScreenViewStateBindings - var displayReactionsMenuForItemId = "" var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)? diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index beb2ceb05..1c67d2dca 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -102,20 +102,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol MXLog.warning("Link clicked: \(url)") case .sendMessage: await sendCurrentMessage() - case .sendReaction(let key, _): - #warning("Reaction implementation awaiting SDK support.") - MXLog.warning("React with \(key) failed. Not implemented.") + case .sendReaction(let emoji, let itemId): + await timelineController.sendReaction(emoji, for: itemId) case .displayEmojiPicker(let itemId): callback?(.displayEmojiPicker(itemId: itemId)) - case .displayReactionsMenuForItemId(let itemId): - state.displayReactionsMenuForItemId = itemId case .cancelReply: state.composerMode = .default case .cancelEdit: state.composerMode = .default - case .emojiTapped(let emoji, let itemId): - await timelineController.sendReaction(emoji, for: itemId) - state.displayReactionsMenuForItemId = "" } } @@ -128,8 +122,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private func paginateBackwards() async { switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) { + case .failure: + displayError(.alert(ElementL10n.roomTimelineBackpaginationFailure)) default: - #warning("Treat errors") + break } } @@ -201,7 +197,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } var actions: [TimelineItemContextMenuAction] = [ - .copy, .quote, .copyPermalink, .reply + .react, .copy, .quote, .copyPermalink, .reply ] if item.isEditable { @@ -215,6 +211,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol return .init(actions: actions) } + // swiftlint:disable:next cyclomatic_complexity private func processContentMenuAction(_ action: TimelineItemContextMenuAction, itemId: String) { guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }), let item = timelineItem as? EventBasedTimelineItemProtocol else { @@ -222,6 +219,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } switch action { + case .react: + callback?(.displayEmojiPicker(itemId: item.id)) case .copy: UIPasteboard.general.string = item.text case .edit: diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index 4376ff07a..4d5477849 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -27,28 +27,25 @@ struct TimelineItemBubbledStylerView: View { @ScaledMetric private var senderNameVerticalPadding = 3 var body: some View { - VStack(alignment: alignment, spacing: -12) { - if !timelineItem.isOutgoing { - header - .zIndex(1) - } - VStack(alignment: alignment) { - if timelineItem.isOutgoing { - HStack { - Spacer() - styledContentWithReactions - if timelineItem.isOutgoing { - TimelineDeliveryStatusView(deliveryStatus: timelineItem.properties.deliveryStatus) - .padding(.top, 6) - } - } - .padding(.trailing, 16) - .padding(.leading, 56) - } else { - styledContentWithReactions - .padding(.leading, 24) - .padding(.trailing, 56) + ZStack(alignment: .trailingFirstTextBaseline) { + VStack(alignment: alignment, spacing: -12) { + if !timelineItem.isOutgoing { + header + .zIndex(1) } + + HStack { + if timelineItem.isOutgoing { + Spacer() + } + + styledContentWithReactions + } + .padding(.horizontal, 16.0) + } + + if timelineItem.isOutgoing { + TimelineDeliveryStatusView(deliveryStatus: timelineItem.properties.deliveryStatus) } } } @@ -85,7 +82,6 @@ struct TimelineItemBubbledStylerView: View { alignment: alignment) { key in context.send(viewAction: .sendReaction(key: key, eventID: timelineItem.id)) } - .padding(.horizontal, 12) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift index 263dcc9a5..4549e01df 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift @@ -28,7 +28,15 @@ struct TimelineItemPlainStylerView: View { header VStack(alignment: .leading, spacing: 4) { - content() + HStack(alignment: .firstTextBaseline) { + content() + + Spacer() + + if timelineItem.isOutgoing { + TimelineDeliveryStatusView(deliveryStatus: timelineItem.properties.deliveryStatus) + } + } supplementaryViews } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineDeliveryStatusView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineDeliveryStatusView.swift index 6d76a6d40..4708d7d7a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineDeliveryStatusView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineDeliveryStatusView.swift @@ -43,17 +43,18 @@ struct TimelineDeliveryStatusView: View { } var body: some View { - if showDeliveryStatus { - Image(systemName: systemImageName) - .task { - if case .sent = deliveryStatus { - try? await Task.sleep(nanoseconds: 1_000_000_000) - withAnimation { - showDeliveryStatus = false - } + Image(systemName: systemImageName) + .resizable() + .frame(width: 12.0, height: 12.0) + .opacity(showDeliveryStatus ? 1.0 : 0.0) + .task { + if case .sent = deliveryStatus { + try? await Task.sleep(nanoseconds: 1_000_000_000) + withAnimation { + showDeliveryStatus = false } } - } + } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemReactionsMenuView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemReactionsMenuView.swift deleted file mode 100644 index b7e5602ec..000000000 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemReactionsMenuView.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// 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 SwiftUI - -struct TimelineItemReactionsMenuView: View { - private let emojis = ["👍🏼", "👎🏼", "😄", "🙏🏼", "😇"] - - var onEmojiSelected: ((String) -> Void)? - var onDisplayEmojiPicker: (() -> Void)? - - var body: some View { - HStack { - HStack(spacing: 10) { - ForEach(emojis, id: \.self) { emoji in - Button { - onEmojiSelected?(emoji) - } label: { - Text(emoji) - } - } - } - .padding(10) - .background(.gray) - .cornerRadius(15) - HStack(spacing: 10) { - Text("➕") - } - .padding(10) - .background(.gray) - .cornerRadius(15) - .onTapGesture { - onDisplayEmojiPicker?() - } - } - } -} - -struct TimelineItemReactionsMenuView_Previews: PreviewProvider { - static var previews: some View { - TimelineItemReactionsMenuView(onDisplayEmojiPicker: nil) - } -} diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift index ad7280519..0ca1f074b 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift @@ -51,7 +51,8 @@ struct FormattedBodyText: View { } } } - .tint(.element.accent) + // Should be .element.accent but that's currently black + .tint(.blue) } private var blockquoteAttributes: AttributeContainer { diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift index b59b48fe7..1b566eabc 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift @@ -22,6 +22,7 @@ struct TimelineItemContextMenuActions { } enum TimelineItemContextMenuAction: Identifiable, Hashable { + case react case copy case edit case quote @@ -59,6 +60,10 @@ public struct TimelineItemContextMenu: View { private func viewsForActions(_ actions: [TimelineItemContextMenuAction]) -> some View { ForEach(actions, id: \.self) { item in switch item { + case .react: + Button { callback(item) } label: { + Label(ElementL10n.reactions, systemImage: "face.smiling") + } case .copy: Button { callback(item) } label: { Label(ElementL10n.actionCopy, systemImage: "doc.on.doc") diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift index dc3602754..e6a8f1a73 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineTableViewController.swift @@ -69,13 +69,7 @@ class TimelineTableViewController: UIViewController { applySnapshot() } } - - var displayReactionsMenuForItemId = "" { - didSet { - tableView.reloadData() - } - } - + var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)? @Binding private var scrollToBottomButtonVisible: Bool @@ -193,7 +187,6 @@ class TimelineTableViewController: UIViewController { // A local reference to avoid capturing self in the cell configuration. let coordinator = self.coordinator let opacity = self.opacity(for: timelineItem) - let displayReactionsMenuForItemId = self.displayReactionsMenuForItemId let contextMenuBuilder = self.contextMenuBuilder cell.item = timelineItem @@ -201,38 +194,28 @@ class TimelineTableViewController: UIViewController { if case .backPaginationIndicator = timelineItem { timelineItem } else { - VStack { - if displayReactionsMenuForItemId == timelineItem.id { - TimelineItemReactionsMenuView { emoji in - coordinator.send(viewAction: .emojiTapped(emoji: emoji, itemId: timelineItem.id)) - } onDisplayEmojiPicker: { - coordinator.send(viewAction: .displayEmojiPicker(itemId: timelineItem.id)) - } + timelineItem + .frame(maxWidth: .infinity, alignment: .leading) + .opacity(opacity) + .contextMenu { + contextMenuBuilder?(timelineItem.id) + } + .onAppear { + coordinator.send(viewAction: .itemAppeared(id: timelineItem.id)) + } + .onDisappear { + coordinator.send(viewAction: .itemDisappeared(id: timelineItem.id)) + } + .environment(\.openURL, OpenURLAction { url in + coordinator.send(viewAction: .linkClicked(url: url)) + return .systemAction + }) + .onTapGesture(count: 2) { + coordinator.send(viewAction: .displayEmojiPicker(itemId: timelineItem.id)) + } + .onTapGesture { + coordinator.send(viewAction: .itemTapped(id: timelineItem.id)) } - - timelineItem - .frame(maxWidth: .infinity, alignment: .leading) - .opacity(opacity) - .contextMenu { - contextMenuBuilder?(timelineItem.id) - } - .onAppear { - coordinator.send(viewAction: .itemAppeared(id: timelineItem.id)) - } - .onDisappear { - coordinator.send(viewAction: .itemDisappeared(id: timelineItem.id)) - } - .environment(\.openURL, OpenURLAction { url in - coordinator.send(viewAction: .linkClicked(url: url)) - return .systemAction - }) - .onTapGesture(count: 2) { - coordinator.send(viewAction: .displayReactionsMenuForItemId(itemId: timelineItem.id)) - } - .onTapGesture { - coordinator.send(viewAction: .itemTapped(id: timelineItem.id)) - } - } } } .margins(.all, self.timelineStyle.rowInsets) @@ -375,15 +358,9 @@ class TimelineTableViewController: UIViewController { extension TimelineTableViewController: UITableViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { - let isAtBottom = isAtBottom() - // Dispatches fix runtime warnings about making changes during a view update. - if !scrollToBottomButtonVisible, isAtBottom { - DispatchQueue.main.async { self.scrollToBottomButtonVisible = true } - } else if scrollToBottomButtonVisible, !isAtBottom { - DispatchQueue.main.async { self.scrollToBottomButtonVisible = false } - } - + DispatchQueue.main.async { self.scrollToBottomButtonVisible = self.isAtBottom() } + paginateBackwardsPublisher.send(()) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift index fdb56c140..f40f0a3dd 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift @@ -65,9 +65,6 @@ struct TimelineView: UIViewControllerRepresentable { if tableViewController.composerMode != context.viewState.composerMode { tableViewController.composerMode = context.viewState.composerMode } - if tableViewController.displayReactionsMenuForItemId != context.viewState.displayReactionsMenuForItemId { - tableViewController.displayReactionsMenuForItemId = context.viewState.displayReactionsMenuForItemId - } // Doesn't have an equatable conformance :( tableViewController.contextMenuBuilder = context.viewState.contextMenuBuilder diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift index ce642c7b4..85d9e9917 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift @@ -95,8 +95,6 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result { do { - Benchmark.startTrackingForIdentifier("Login", message: "Started new login") - let client = try await Task.dispatch(on: .global()) { try self.authenticationService.login(username: username, password: password, @@ -104,11 +102,8 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { deviceId: deviceId) } - Benchmark.endTrackingForIdentifier("Login", message: "Finished login") return await userSession(for: client) } catch { - Benchmark.endTrackingForIdentifier("Login", message: "Login failed") - MXLog.error("Failed logging in with error: \(error)") guard let error = error as? AuthenticationError else { return .failure(.failedLoggingIn) } diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 158880bca..6eccb5d76 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -27,7 +27,7 @@ class RoomProxy: RoomProxyProtocol { private let serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.serial") - private var sendMessageBgTask: BackgroundTaskProtocol? + private var sendMessageBackgroundTask: BackgroundTaskProtocol? private var memberAvatars = [String: String]() private var memberDisplayNames = [String: String]() @@ -151,10 +151,7 @@ class RoomProxy: RoomProxyProtocol { do { let outcome: PaginationOutcome = try await Task.dispatch(on: .global()) { - Benchmark.startTrackingForIdentifier("BackPagination \(id)", message: "Backpaginating \(count) message(s) in room \(id)") - let outcome = try self.room.paginateBackwards(limit: UInt16(count)) - Benchmark.endTrackingForIdentifier("BackPagination \(id)", message: "Finished backpaginating \(count) message(s) in room \(id)") - return outcome + try self.room.paginateBackwards(limit: UInt16(count)) } update(backPaginationOutcome: outcome) return .success(()) @@ -164,9 +161,9 @@ class RoomProxy: RoomProxyProtocol { } func sendMessage(_ message: String, inReplyToEventId: String? = nil) async -> Result { - sendMessageBgTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true) + sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true) defer { - sendMessageBgTask?.stop() + sendMessageBackgroundTask?.stop() } let transactionId = genTransactionId() @@ -187,15 +184,25 @@ class RoomProxy: RoomProxyProtocol { } func sendReaction(_ reaction: String, for eventId: String) async -> Result { - await Task.dispatch(on: .global()) { - .success(()) + sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + return await Task.dispatch(on: .global()) { + do { + try self.room.sendReaction(eventId: eventId, key: reaction) + return .success(()) + } catch { + return .failure(.failedSendingReaction) + } } } func editMessage(_ newMessage: String, originalEventId: String) async -> Result { - sendMessageBgTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true) + sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true) defer { - sendMessageBgTask?.stop() + sendMessageBackgroundTask?.stop() } let transactionId = genTransactionId() @@ -205,7 +212,7 @@ class RoomProxy: RoomProxyProtocol { try self.room.edit(newMsg: newMessage, originalEventId: originalEventId, txnId: transactionId) return .success(()) } catch { - return .failure(.failedSendingMessage) + return .failure(.failedEditingMessage) } } } diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index b5a842ce3..d1f42d333 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -23,6 +23,8 @@ enum RoomProxyError: Error { case failedRetrievingMemberAvatarURL case failedRetrievingMemberDisplayName case failedSendingMessage + case failedSendingReaction + case failedEditingMessage case failedRedactingEvent case failedAddingTimelineListener case failedRetrievingMembers diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index 24f89491c..3b049e7fa 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -161,8 +161,8 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { return buildEmptyRoomSummary() case .filled(let roomId): return buildRoomSummaryForIdentifier(roomId) - case .invalidated: - return buildEmptyRoomSummary() + case .invalidated(let roomId): + return buildRoomSummaryForIdentifier(roomId) } } diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index bf17b1457..260a7abed 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -64,7 +64,7 @@ enum RoomTimelineItemFixtures { senderId: "", senderDisplayName: "Helena"), TextRoomTimelineItem(id: UUID().uuidString, - text: "And John's speech was amazing!", + text: "And John's speech was amazing!And John's speech was amazing!And John's speech was amazing!", timestamp: "5 PM", inGroupState: .beginning, isOutgoing: true, diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index 229ef1d37..427498d1b 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -100,8 +100,6 @@ class UserSessionStore: UserSessionStoreProtocol { // MARK: - Private private func restorePreviousLogin(_ credentials: KeychainCredentials) async -> Result { - Benchmark.startTrackingForIdentifier("Login", message: "Started restoring previous login") - let builder = ClientBuilder() .basePath(path: baseDirectory.path) .username(username: credentials.userID) diff --git a/UnitTests/Sources/ImageAnonymizerTests.swift b/UnitTests/Sources/ImageAnonymizerTests.swift deleted file mode 100644 index 515eb782d..000000000 --- a/UnitTests/Sources/ImageAnonymizerTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// 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 - -enum ImageAnonymizerTestsError: String, Error { - case screenshotNotFound -} - -class ImageAnonymizerTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func sampleScreenshot() throws -> UIImage { - let bundle = Bundle(for: classForCoder) - guard let path = bundle.path(forResource: "sample_screenshot", ofType: "png"), - let image = UIImage(contentsOfFile: path) else { - throw ImageAnonymizerTestsError.screenshotNotFound - } - return image - } - - func testImageAnonymizationConfidenceLevel() async throws { - let image = try sampleScreenshot() - - let anonymized5 = try await ImageAnonymizer.anonymizedImage(from: image) - let anonymized1 = try await ImageAnonymizer.anonymizedImage(from: image, confidenceLevel: 0.1) - - // comparing colors is a complicated process, just compare images for now - XCTAssertNotEqual(image, anonymized5) - XCTAssertNotEqual(anonymized1, anonymized5) - } - - func testImageAnonymizationFillColor() async throws { - let image = try sampleScreenshot() - - let anonymizedRed = try await ImageAnonymizer.anonymizedImage(from: image) - let anonymizedBlue = try await ImageAnonymizer.anonymizedImage(from: image, fillColor: .blue) - - // comparing colors is a complicated process, just compare images for now - XCTAssertNotEqual(image, anonymizedRed) - XCTAssertNotEqual(anonymizedBlue, anonymizedRed) - } -} diff --git a/changelog.d/pr-381.bugfix b/changelog.d/pr-381.bugfix new file mode 100644 index 000000000..d1c17f9d4 --- /dev/null +++ b/changelog.d/pr-381.bugfix @@ -0,0 +1,9 @@ +* moved the message delivery status outside of the main content and added it to the plain timeline as well +* fixed glithcy scroll to bottom timeline button +* simplified the emoji picker, double tapping a timeline item directly opens it now and added a context menu option. Linked it to rust side reaction sending +* fixed cold cache seemingly not working (invalid rooms treated as empty) +* made splash screen full screen +* fixed connectivity indicator starting off as offline +* added presentation detents on the NavigationStackCoordinator as they're not inherited from the child +* fixed timeline item link tint colors +* removed some unnecessary classes \ No newline at end of file