mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Various tweaks (#381)
* Add the message delivery status to the plain timeline styler and display it outside of the main content * Fix glitchy timeline scroll to bottom button * Fixed bubbled reactions padding * 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 * Fix the sliding sync cold cache: treat invalidated rooms as filled * Make splash screen view full screen * Fix the offline indicator popping up when first setting up the network monitor * Expose presentationDetents on the NavigationStackCoordinator and start using them for the EmojiPicker * Fix link tint color * Linked TimelineReactionsView reaction sending * Remove now unused/unnecessary classes * Add changelog * Fix formatting issue
This commit is contained in:
parent
5b9d45e461
commit
71f1f044c7
@ -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 = "<group>"; };
|
||||
1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
|
||||
124D85E85505B6B81845235F /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fy; path = fy.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizer.swift; sourceTree = "<group>"; };
|
||||
13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||
142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetails.swift; sourceTree = "<group>"; };
|
||||
@ -675,7 +669,6 @@
|
||||
4990FDBDA96B88E214F92F48 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
|
||||
49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = "<group>"; };
|
||||
49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmark.swift; sourceTree = "<group>"; };
|
||||
4A57A4AFA6A068668AFBD070 /* UIActivityViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityViewControllerWrapper.swift; sourceTree = "<group>"; };
|
||||
4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettings.swift; sourceTree = "<group>"; };
|
||||
4B40B7F6FCCE2D8C242492D9 /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@ -716,7 +709,6 @@
|
||||
5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = "<group>"; };
|
||||
5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = "<group>"; };
|
||||
6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizerTests.swift; sourceTree = "<group>"; };
|
||||
607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageProtocol.swift; sourceTree = "<group>"; };
|
||||
616197D81103330BF2ADD559 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@ -866,7 +858,6 @@
|
||||
ACA11F7F50A4A3887A18CA5A /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewProvider.swift; sourceTree = "<group>"; };
|
||||
AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
AD61385D39FD330AFB928E6A /* TimelineItemReactionsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReactionsMenuView.swift; sourceTree = "<group>"; };
|
||||
AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportCoordinator.swift; sourceTree = "<group>"; };
|
||||
ADB3A7BCE745626EC61EF3C3 /* FilePreviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewCoordinator.swift; sourceTree = "<group>"; };
|
||||
ADCB8A232D3A8FB3E16A7303 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@ -912,7 +903,6 @@
|
||||
C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
C483956FA3D665E3842E319A /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = "<group>"; };
|
||||
C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
|
||||
C615E29435BC651DD2E95B67 /* EmojiPickerSearchFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerSearchFieldView.swift; sourceTree = "<group>"; };
|
||||
C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = "<group>"; };
|
||||
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = "<group>"; };
|
||||
C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@ -1269,7 +1259,6 @@
|
||||
children = (
|
||||
AB785716B9212C093704E767 /* EmojiPickerHeaderView.swift */,
|
||||
00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */,
|
||||
C615E29435BC651DD2E95B67 /* EmojiPickerSearchFieldView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
@ -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 */,
|
||||
|
@ -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";
|
||||
|
@ -377,6 +377,8 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
||||
}
|
||||
}
|
||||
|
||||
var presentationDetents: Set<PresentationDetent> = []
|
||||
|
||||
// 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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ class NetworkMonitor {
|
||||
init() {
|
||||
queue = DispatchQueue(label: "io.element.elementx.networkmonitor")
|
||||
pathMonitor = NWPathMonitor()
|
||||
reachabilityPublisher = CurrentValueSubject<Bool, Never>(pathMonitor.currentPath.status == .satisfied)
|
||||
reachabilityPublisher = CurrentValueSubject<Bool, Never>(true)
|
||||
|
||||
pathMonitor.pathUpdateHandler = { [weak self] path in
|
||||
DispatchQueue.main.async {
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(""))
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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)?
|
||||
|
||||
|
@ -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:
|
||||
|
@ -27,28 +27,25 @@ struct TimelineItemBubbledStylerView<Content: View>: 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<Content: View>: View {
|
||||
alignment: alignment) { key in
|
||||
context.send(viewAction: .sendReaction(key: key, eventID: timelineItem.id))
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,15 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
header
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
content()
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
content()
|
||||
|
||||
Spacer()
|
||||
|
||||
if timelineItem.isOutgoing {
|
||||
TimelineDeliveryStatusView(deliveryStatus: timelineItem.properties.deliveryStatus)
|
||||
}
|
||||
}
|
||||
supplementaryViews
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -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(())
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -95,8 +95,6 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
|
||||
func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
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) }
|
||||
|
||||
|
@ -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<Void, RoomProxyError> {
|
||||
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<Void, RoomProxyError> {
|
||||
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<Void, RoomProxyError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ enum RoomProxyError: Error {
|
||||
case failedRetrievingMemberAvatarURL
|
||||
case failedRetrievingMemberDisplayName
|
||||
case failedSendingMessage
|
||||
case failedSendingReaction
|
||||
case failedEditingMessage
|
||||
case failedRedactingEvent
|
||||
case failedAddingTimelineListener
|
||||
case failedRetrievingMembers
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -100,8 +100,6 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
// MARK: - Private
|
||||
|
||||
private func restorePreviousLogin(_ credentials: KeychainCredentials) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
|
||||
Benchmark.startTrackingForIdentifier("Login", message: "Started restoring previous login")
|
||||
|
||||
let builder = ClientBuilder()
|
||||
.basePath(path: baseDirectory.path)
|
||||
.username(username: credentials.userID)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
9
changelog.d/pr-381.bugfix
Normal file
9
changelog.d/pr-381.bugfix
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user