Finish v0.9.2

This commit is contained in:
manuroe 2019-08-08 18:20:49 +02:00
commit 5a30474f4d
116 changed files with 5066 additions and 1261 deletions

View file

@ -1,3 +1,20 @@
Changes in 0.9.2 (2019-08-08)
===============================================
Improvements:
* Upgrade MatrixKit version ([v0.10.2](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.10.2)).
* Soft logout: Support soft logout (#2540).
* Reactions: Emoji picker (#2370).
* Widgets: Whitelist https://scalar-staging.vector.im/api (#2612).
* Reactions: Show who reacted (#2591).
* Media picking: Use native camera and use separate actions for camera and media picker (#638).
Bug fix:
* Crash when leaving settings due to backup section refresh animation.
* Reactions: Do not display reactions on redacted events in timeline.
* Fix crash for search bar customisation in iOS13 (#2626).
* Build: Fix build based on git tag.
Changes in 0.9.1 (2019-07-17)
===============================================

View file

@ -7,7 +7,7 @@ use_frameworks!
# Different flavours of pods to MatrixKit
# The current MatrixKit pod version
$matrixKitVersion = '0.10.1'
$matrixKitVersion = '0.10.2'
# The develop branch version
#$matrixKitVersion = 'develop'
@ -79,6 +79,10 @@ abstract_target 'RiotPods' do
target "Riot" do
import_MatrixKit
pod 'DGCollectionViewLeftAlignFlowLayout', '~> 1.0.4'
target 'RiotTests' do
inherit! :search_paths
end
end
target "RiotShareExtension" do

View file

@ -49,41 +49,41 @@ PODS:
- MatomoTracker (6.0.1):
- MatomoTracker/Core (= 6.0.1)
- MatomoTracker/Core (6.0.1)
- MatrixKit (0.10.1):
- MatrixKit (0.10.2):
- cmark (~> 0.24.1)
- DTCoreText (~> 1.6.21)
- HPGrowingTextView (~> 1.1)
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixKit/Core (= 0.10.1)
- MatrixSDK (= 0.13.0)
- MatrixKit/Core (= 0.10.2)
- MatrixSDK (= 0.13.1)
- SwiftUTI (~> 1.0.6)
- MatrixKit/AppExtension (0.10.1):
- MatrixKit/AppExtension (0.10.2):
- cmark (~> 0.24.1)
- DTCoreText (~> 1.6.21)
- DTCoreText/Extension
- HPGrowingTextView (~> 1.1)
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixSDK (= 0.13.0)
- MatrixSDK (= 0.13.1)
- SwiftUTI (~> 1.0.6)
- MatrixKit/Core (0.10.1):
- MatrixKit/Core (0.10.2):
- cmark (~> 0.24.1)
- DTCoreText (~> 1.6.21)
- HPGrowingTextView (~> 1.1)
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixSDK (= 0.13.0)
- MatrixSDK (= 0.13.1)
- SwiftUTI (~> 1.0.6)
- MatrixSDK (0.13.0):
- MatrixSDK/Core (= 0.13.0)
- MatrixSDK/Core (0.13.0):
- MatrixSDK (0.13.1):
- MatrixSDK/Core (= 0.13.1)
- MatrixSDK/Core (0.13.1):
- AFNetworking (~> 3.2.0)
- GZIP (~> 1.2.2)
- libbase58 (~> 0.1.4)
- OLMKit (~> 3.1.0)
- Realm (~> 3.13.1)
- MatrixSDK/JingleCallStack (0.13.0):
- MatrixSDK/JingleCallStack (0.13.1):
- JitsiMeetSDK (~> 2.1.0)
- MatrixSDK/Core
- MatrixSDK/SwiftSupport (0.13.0):
- MatrixSDK/SwiftSupport (0.13.1):
- MatrixSDK/Core
- OLMKit (3.1.0):
- OLMKit/olmc (= 3.1.0)
@ -109,8 +109,8 @@ DEPENDENCIES:
- DTCoreText
- GBDeviceInfo (~> 5.2.0)
- MatomoTracker (~> 6.0.1)
- MatrixKit (= 0.10.1)
- MatrixKit/AppExtension (= 0.10.1)
- MatrixKit (= 0.10.2)
- MatrixKit/AppExtension (= 0.10.2)
- MatrixSDK/JingleCallStack
- MatrixSDK/SwiftSupport
- OLMKit
@ -166,8 +166,8 @@ SPEC CHECKSUMS:
libbase58: 7c040313537b8c44b6e2d15586af8e21f7354efd
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
MatomoTracker: 3ae4f65a1f5ace8043bda7244888fee28a734de5
MatrixKit: f8224de32ca8b6e4c54a2654369cedec7744dc6d
MatrixSDK: 6886e7234c650408db5876b44a7f7608c865ce30
MatrixKit: 208801ba995442a6daa913eb6d639bb918c91c24
MatrixSDK: 16c8c4b530a7f9fb264baced52b6b0948b1d0a98
OLMKit: 4ee0159d63feeb86d836fdcfefe418e163511639
Realm: 50071da38fe079e0735e47c9f2eae738c68c5996
Reusable: 82be188f29d96dc5eff0db7b2393bcc08d2cdd5b
@ -176,6 +176,6 @@ SPEC CHECKSUMS:
SwiftUTI: 917993c124f8eac25e88ced0202fc58d7eb50fa8
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
PODFILE CHECKSUM: 6b3ff49b9c446763a5629e71bdde3fe8da7ba93f
PODFILE CHECKSUM: 131195037301d9bfb7465da86b3ea79aac686b74
COCOAPODS: 1.7.2

View file

@ -101,6 +101,7 @@
32F6B96C2270623100BBA352 /* DeviceVerificationDataLoadingCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9662270623100BBA352 /* DeviceVerificationDataLoadingCoordinatorType.swift */; };
32F6B96D2270623100BBA352 /* DeviceVerificationDataLoadingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9672270623100BBA352 /* DeviceVerificationDataLoadingViewModel.swift */; };
32F6B96E2270623100BBA352 /* DeviceVerificationDataLoadingViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9682270623100BBA352 /* DeviceVerificationDataLoadingViewModelType.swift */; };
3AF393339D2D566CE14AC200 /* Pods_RiotTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 129EB7E27E7E4AC3F5F098F5 /* Pods_RiotTests.framework */; };
405FD41D306133A48D9B5AA1 /* Pods_RiotPods_Riot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ACF09217ADF1D7E7A35BC02 /* Pods_RiotPods_Riot.framework */; };
670966FEFE120D865FD8A5B6 /* Pods_RiotPods_SiriIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51187E952D5CECF6D6F5A28E /* Pods_RiotPods_SiriIntents.framework */; };
926FA53F1F4C132000F826C2 /* MXSession+Riot.m in Sources */ = {isa = PBXBuildFile; fileRef = 926FA53E1F4C132000F826C2 /* MXSession+Riot.m */; };
@ -144,6 +145,7 @@
B110872421F098F0003554A5 /* ActivityIndicatorView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B110872021F098EF003554A5 /* ActivityIndicatorView.xib */; };
B110872521F098F0003554A5 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B110872121F098EF003554A5 /* ActivityIndicatorPresenter.swift */; };
B110872621F098F0003554A5 /* ActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B110872221F098F0003554A5 /* ActivityIndicatorView.swift */; };
B120863722EF375F001F89E0 /* ReactionHistoryBridgeCoordinatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B120863622EF375F001F89E0 /* ReactionHistoryBridgeCoordinatorPresenter.swift */; };
B139C21B21FE5B9200BB68EC /* KeyBackupRecoverFromPassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B139C21A21FE5B9100BB68EC /* KeyBackupRecoverFromPassphraseViewModel.swift */; };
B139C21D21FE5BF500BB68EC /* KeyBackupRecoverFromPassphraseViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B139C21C21FE5BF500BB68EC /* KeyBackupRecoverFromPassphraseViewModelType.swift */; };
B139C21F21FE5D6600BB68EC /* KeyBackupRecoverFromPassphraseViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B139C21E21FE5D6600BB68EC /* KeyBackupRecoverFromPassphraseViewAction.swift */; };
@ -163,6 +165,8 @@
B14F143322144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14F142B22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModel.swift */; };
B14F143422144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14F142C22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewAction.swift */; };
B14F143522144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14F142D22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.swift */; };
B152C73122DF561E0041315A /* EmojiServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B152C73022DF561E0041315A /* EmojiServiceTests.swift */; };
B152C73522DF599C0041315A /* apple_emojis_data.json in Resources */ = {isa = PBXBuildFile; fileRef = B152C73422DF599B0041315A /* apple_emojis_data.json */; };
B1664BC520F4E67600808783 /* FallbackViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1664BAD20F4E67500808783 /* FallbackViewController.xib */; };
B1664BC620F4E67600808783 /* FallbackViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1664BAE20F4E67500808783 /* FallbackViewController.m */; };
B1664BC720F4E67600808783 /* SharePresentingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1664BB220F4E67500808783 /* SharePresentingViewController.m */; };
@ -457,6 +461,25 @@
B1B5599320EFC5E400210D55 /* DecryptionFailure.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5598D20EFC5E400210D55 /* DecryptionFailure.m */; };
B1B5599420EFC5E400210D55 /* DecryptionFailureTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5599120EFC5E400210D55 /* DecryptionFailureTracker.m */; };
B1B9194C2118984300FE25B5 /* RoomPredecessorBubbleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1B9194A2118984300FE25B5 /* RoomPredecessorBubbleCell.xib */; };
B1B9DEDA22E9B7350065E677 /* SerializationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DED922E9B7350065E677 /* SerializationService.swift */; };
B1B9DEDC22E9B7440065E677 /* SerializationServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEDB22E9B7440065E677 /* SerializationServiceType.swift */; };
B1B9DEDE22E9D9890065E677 /* EmojiServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEDD22E9D9890065E677 /* EmojiServiceType.swift */; };
B1B9DEE822EB34EF0065E677 /* ReactionHistoryCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEE022EB34ED0065E677 /* ReactionHistoryCoordinatorType.swift */; };
B1B9DEE922EB34EF0065E677 /* ReactionHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEE122EB34EE0065E677 /* ReactionHistoryViewController.swift */; };
B1B9DEEA22EB34EF0065E677 /* ReactionHistoryViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B1B9DEE222EB34EE0065E677 /* ReactionHistoryViewController.storyboard */; };
B1B9DEEB22EB34EF0065E677 /* ReactionHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEE322EB34EE0065E677 /* ReactionHistoryViewModel.swift */; };
B1B9DEEC22EB34EF0065E677 /* ReactionHistoryViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEE422EB34EE0065E677 /* ReactionHistoryViewModelType.swift */; };
B1B9DEED22EB34EF0065E677 /* ReactionHistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEE522EB34EE0065E677 /* ReactionHistoryCoordinator.swift */; };
B1B9DEEE22EB34EF0065E677 /* ReactionHistoryViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEE622EB34EE0065E677 /* ReactionHistoryViewAction.swift */; };
B1B9DEEF22EB34EF0065E677 /* ReactionHistoryViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEE722EB34EE0065E677 /* ReactionHistoryViewState.swift */; };
B1B9DEF122EB396B0065E677 /* ReactionHistoryViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEF022EB396B0065E677 /* ReactionHistoryViewData.swift */; };
B1B9DEF422EB426D0065E677 /* ReactionHistoryViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B9DEF222EB426D0065E677 /* ReactionHistoryViewCell.swift */; };
B1B9DEF522EB426D0065E677 /* ReactionHistoryViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1B9DEF322EB426D0065E677 /* ReactionHistoryViewCell.xib */; };
B1C335CD22F1C1320021BA8D /* CameraPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C335CC22F1C1320021BA8D /* CameraPresenter.swift */; };
B1C3360122F1ED600021BA8D /* MediaPickerCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C335FE22F1ED5F0021BA8D /* MediaPickerCoordinatorType.swift */; };
B1C3360222F1ED600021BA8D /* MediaPickerCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C335FF22F1ED5F0021BA8D /* MediaPickerCoordinatorBridgePresenter.swift */; };
B1C3360322F1ED600021BA8D /* MediaPickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C3360022F1ED600021BA8D /* MediaPickerCoordinator.swift */; };
B1C3361C22F32B4A0021BA8D /* SingleImagePickerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C3361B22F32B4A0021BA8D /* SingleImagePickerPresenter.swift */; };
B1C562CA2289C2690037F12A /* UIGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562C92289C2690037F12A /* UIGestureRecognizer.swift */; };
B1C562CC228AB3510037F12A /* UIStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562CB228AB3510037F12A /* UIStackView.swift */; };
B1C562D9228C0B760037F12A /* RoomContextualMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562D8228C0B760037F12A /* RoomContextualMenuItem.swift */; };
@ -489,6 +512,30 @@
B1DB4F0C2231494F0065DBFA /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DB4F0A223131600065DBFA /* String.swift */; };
B1DB4F0E22316FFF0065DBFA /* UserNameColorGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DB4F0D22316FFF0065DBFA /* UserNameColorGenerator.swift */; };
B1DB4F0F223170000065DBFA /* UserNameColorGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DB4F0D22316FFF0065DBFA /* UserNameColorGenerator.swift */; };
B1DCC61722E5E17100625807 /* EmojiPickerViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B1DCC60F22E5E16900625807 /* EmojiPickerViewController.storyboard */; };
B1DCC61822E5E17100625807 /* EmojiPickerViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC61022E5E16900625807 /* EmojiPickerViewState.swift */; };
B1DCC61922E5E17100625807 /* EmojiPickerCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC61122E5E16A00625807 /* EmojiPickerCoordinatorType.swift */; };
B1DCC61A22E5E17100625807 /* EmojiPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC61222E5E16C00625807 /* EmojiPickerViewController.swift */; };
B1DCC61B22E5E17100625807 /* EmojiPickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC61322E5E16D00625807 /* EmojiPickerCoordinator.swift */; };
B1DCC61C22E5E17100625807 /* EmojiPickerViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC61422E5E16E00625807 /* EmojiPickerViewAction.swift */; };
B1DCC61D22E5E17100625807 /* EmojiPickerViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC61522E5E16F00625807 /* EmojiPickerViewModelType.swift */; };
B1DCC61E22E5E17100625807 /* EmojiPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC61622E5E17000625807 /* EmojiPickerViewModel.swift */; };
B1DCC62022E5EDA400625807 /* EmojiPickerCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC61F22E5EDA400625807 /* EmojiPickerCoordinatorBridgePresenter.swift */; };
B1DCC62222E60BE000625807 /* EmojiPickerItemViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC62122E60BE000625807 /* EmojiPickerItemViewData.swift */; };
B1DCC62422E60CA900625807 /* EmojiPickerCategoryViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC62322E60CA800625807 /* EmojiPickerCategoryViewData.swift */; };
B1DCC62622E60CC600625807 /* EmojiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC62522E60CC600625807 /* EmojiItem.swift */; };
B1DCC62822E60CE300625807 /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC62722E60CE300625807 /* EmojiCategory.swift */; };
B1DCC62A22E60D1000625807 /* EmojiMartService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC62922E60D1000625807 /* EmojiMartService.swift */; };
B1DCC62D22E61EAF00625807 /* EmojiPickerViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC62B22E61EAF00625807 /* EmojiPickerViewCell.swift */; };
B1DCC62E22E61EAF00625807 /* EmojiPickerViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1DCC62C22E61EAF00625807 /* EmojiPickerViewCell.xib */; };
B1DCC63122E7026F00625807 /* EmojiPickerHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC62F22E7026F00625807 /* EmojiPickerHeaderView.swift */; };
B1DCC63222E7026F00625807 /* EmojiPickerHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1DCC63022E7026F00625807 /* EmojiPickerHeaderView.xib */; };
B1DCC63422E72C1B00625807 /* UISearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC63322E72C1B00625807 /* UISearchBar.swift */; };
B1DCC63522E72D2200625807 /* UISearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC63322E72C1B00625807 /* UISearchBar.swift */; };
B1DCC63722E8541700625807 /* EmojiStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC63622E8541700625807 /* EmojiStore.swift */; };
B1DCC63922E85E9A00625807 /* EmojiMartStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC63822E85E9A00625807 /* EmojiMartStore.swift */; };
B1DCC63B22E85EF800625807 /* EmojiMartCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC63A22E85EF800625807 /* EmojiMartCategory.swift */; };
B1DCC63F22E9A3AE00625807 /* EmojiItem+EmojiMart.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DCC63E22E9A3AE00625807 /* EmojiItem+EmojiMart.swift */; };
B1E5368921FB1E20001F3AFF /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E5368821FB1E20001F3AFF /* UIButton.swift */; };
B1E5368D21FB7245001F3AFF /* KeyBackupRecoverFromPassphraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E5368C21FB7245001F3AFF /* KeyBackupRecoverFromPassphraseViewController.swift */; };
B1E5368F21FB7258001F3AFF /* KeyBackupRecoverFromPassphraseViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B1E5368E21FB7258001F3AFF /* KeyBackupRecoverFromPassphraseViewController.storyboard */; };
@ -558,6 +605,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
129EB7E27E7E4AC3F5F098F5 /* Pods_RiotTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RiotTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1ACF09217ADF1D7E7A35BC02 /* Pods_RiotPods_Riot.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RiotPods_Riot.framework; sourceTree = BUILT_PRODUCTS_DIR; };
24CBEC4E1F0EAD310093EABB /* RiotShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RiotShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
3209451121F1C1430088CAA2 /* BlackTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlackTheme.swift; sourceTree = "<group>"; };
@ -679,6 +727,7 @@
92726A501F587410004AD26F /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
97151D7F0F892081250D50A3 /* Pods_RiotPods_RiotShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RiotPods_RiotShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9C3242E3FE95BCDA9562C75D /* Pods-RiotPods-RiotShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotPods-RiotShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-RiotPods-RiotShareExtension/Pods-RiotPods-RiotShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
AC34BF67FD21A9D01C16AE5D /* Pods-RiotTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotTests.release.xcconfig"; path = "Target Support Files/Pods-RiotTests/Pods-RiotTests.release.xcconfig"; sourceTree = "<group>"; };
B104C2932203773B00D9F496 /* KeyBackupBannerPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyBackupBannerPreferences.swift; sourceTree = "<group>"; };
B1057788221304EB00334B1E /* KeyBackupSetupSuccessFromPassphraseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupSetupSuccessFromPassphraseViewController.swift; sourceTree = "<group>"; };
B105778A221304FA00334B1E /* KeyBackupSetupSuccessFromPassphraseViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = KeyBackupSetupSuccessFromPassphraseViewController.storyboard; sourceTree = "<group>"; };
@ -715,6 +764,7 @@
B110872021F098EF003554A5 /* ActivityIndicatorView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ActivityIndicatorView.xib; sourceTree = "<group>"; };
B110872121F098EF003554A5 /* ActivityIndicatorPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenter.swift; sourceTree = "<group>"; };
B110872221F098F0003554A5 /* ActivityIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorView.swift; sourceTree = "<group>"; };
B120863622EF375F001F89E0 /* ReactionHistoryBridgeCoordinatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionHistoryBridgeCoordinatorPresenter.swift; sourceTree = "<group>"; };
B139C21A21FE5B9100BB68EC /* KeyBackupRecoverFromPassphraseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromPassphraseViewModel.swift; sourceTree = "<group>"; };
B139C21C21FE5BF500BB68EC /* KeyBackupRecoverFromPassphraseViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromPassphraseViewModelType.swift; sourceTree = "<group>"; };
B139C21E21FE5D6600BB68EC /* KeyBackupRecoverFromPassphraseViewAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromPassphraseViewAction.swift; sourceTree = "<group>"; };
@ -734,6 +784,9 @@
B14F142B22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromRecoveryKeyViewModel.swift; sourceTree = "<group>"; };
B14F142C22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromRecoveryKeyViewAction.swift; sourceTree = "<group>"; };
B14F142D22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromRecoveryKeyViewController.swift; sourceTree = "<group>"; };
B152C72D22DF55D80041315A /* RiotTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RiotTests-Bridging-Header.h"; sourceTree = "<group>"; };
B152C73022DF561E0041315A /* EmojiServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiServiceTests.swift; sourceTree = "<group>"; };
B152C73422DF599B0041315A /* apple_emojis_data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apple_emojis_data.json; sourceTree = "<group>"; };
B1664BAD20F4E67500808783 /* FallbackViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = FallbackViewController.xib; sourceTree = "<group>"; };
B1664BAE20F4E67500808783 /* FallbackViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FallbackViewController.m; sourceTree = "<group>"; };
B1664BAF20F4E67500808783 /* FallbackViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FallbackViewController.h; sourceTree = "<group>"; };
@ -1212,6 +1265,25 @@
B1B5599020EFC5E400210D55 /* Analytics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Analytics.h; sourceTree = "<group>"; };
B1B5599120EFC5E400210D55 /* DecryptionFailureTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DecryptionFailureTracker.m; sourceTree = "<group>"; };
B1B9194A2118984300FE25B5 /* RoomPredecessorBubbleCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RoomPredecessorBubbleCell.xib; sourceTree = "<group>"; };
B1B9DED922E9B7350065E677 /* SerializationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerializationService.swift; sourceTree = "<group>"; };
B1B9DEDB22E9B7440065E677 /* SerializationServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerializationServiceType.swift; sourceTree = "<group>"; };
B1B9DEDD22E9D9890065E677 /* EmojiServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiServiceType.swift; sourceTree = "<group>"; };
B1B9DEE022EB34ED0065E677 /* ReactionHistoryCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionHistoryCoordinatorType.swift; sourceTree = "<group>"; };
B1B9DEE122EB34EE0065E677 /* ReactionHistoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionHistoryViewController.swift; sourceTree = "<group>"; };
B1B9DEE222EB34EE0065E677 /* ReactionHistoryViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ReactionHistoryViewController.storyboard; sourceTree = "<group>"; };
B1B9DEE322EB34EE0065E677 /* ReactionHistoryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionHistoryViewModel.swift; sourceTree = "<group>"; };
B1B9DEE422EB34EE0065E677 /* ReactionHistoryViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionHistoryViewModelType.swift; sourceTree = "<group>"; };
B1B9DEE522EB34EE0065E677 /* ReactionHistoryCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionHistoryCoordinator.swift; sourceTree = "<group>"; };
B1B9DEE622EB34EE0065E677 /* ReactionHistoryViewAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionHistoryViewAction.swift; sourceTree = "<group>"; };
B1B9DEE722EB34EE0065E677 /* ReactionHistoryViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionHistoryViewState.swift; sourceTree = "<group>"; };
B1B9DEF022EB396B0065E677 /* ReactionHistoryViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionHistoryViewData.swift; sourceTree = "<group>"; };
B1B9DEF222EB426D0065E677 /* ReactionHistoryViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionHistoryViewCell.swift; sourceTree = "<group>"; };
B1B9DEF322EB426D0065E677 /* ReactionHistoryViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReactionHistoryViewCell.xib; sourceTree = "<group>"; };
B1C335CC22F1C1320021BA8D /* CameraPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPresenter.swift; sourceTree = "<group>"; };
B1C335FE22F1ED5F0021BA8D /* MediaPickerCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPickerCoordinatorType.swift; sourceTree = "<group>"; };
B1C335FF22F1ED5F0021BA8D /* MediaPickerCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPickerCoordinatorBridgePresenter.swift; sourceTree = "<group>"; };
B1C3360022F1ED600021BA8D /* MediaPickerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPickerCoordinator.swift; sourceTree = "<group>"; };
B1C3361B22F32B4A0021BA8D /* SingleImagePickerPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleImagePickerPresenter.swift; sourceTree = "<group>"; };
B1C562C92289C2690037F12A /* UIGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIGestureRecognizer.swift; sourceTree = "<group>"; };
B1C562CB228AB3510037F12A /* UIStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackView.swift; sourceTree = "<group>"; };
B1C562D8228C0B760037F12A /* RoomContextualMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuItem.swift; sourceTree = "<group>"; };
@ -1242,11 +1314,35 @@
B1DB4F05223015080065DBFA /* Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Character.swift; sourceTree = "<group>"; };
B1DB4F0A223131600065DBFA /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
B1DB4F0D22316FFF0065DBFA /* UserNameColorGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNameColorGenerator.swift; sourceTree = "<group>"; };
B1DCC60F22E5E16900625807 /* EmojiPickerViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = EmojiPickerViewController.storyboard; sourceTree = "<group>"; };
B1DCC61022E5E16900625807 /* EmojiPickerViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewState.swift; sourceTree = "<group>"; };
B1DCC61122E5E16A00625807 /* EmojiPickerCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerCoordinatorType.swift; sourceTree = "<group>"; };
B1DCC61222E5E16C00625807 /* EmojiPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewController.swift; sourceTree = "<group>"; };
B1DCC61322E5E16D00625807 /* EmojiPickerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerCoordinator.swift; sourceTree = "<group>"; };
B1DCC61422E5E16E00625807 /* EmojiPickerViewAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewAction.swift; sourceTree = "<group>"; };
B1DCC61522E5E16F00625807 /* EmojiPickerViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewModelType.swift; sourceTree = "<group>"; };
B1DCC61622E5E17000625807 /* EmojiPickerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewModel.swift; sourceTree = "<group>"; };
B1DCC61F22E5EDA400625807 /* EmojiPickerCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerCoordinatorBridgePresenter.swift; sourceTree = "<group>"; };
B1DCC62122E60BE000625807 /* EmojiPickerItemViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerItemViewData.swift; sourceTree = "<group>"; };
B1DCC62322E60CA800625807 /* EmojiPickerCategoryViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerCategoryViewData.swift; sourceTree = "<group>"; };
B1DCC62522E60CC600625807 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = "<group>"; };
B1DCC62722E60CE300625807 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = "<group>"; };
B1DCC62922E60D1000625807 /* EmojiMartService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartService.swift; sourceTree = "<group>"; };
B1DCC62B22E61EAF00625807 /* EmojiPickerViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerViewCell.swift; sourceTree = "<group>"; };
B1DCC62C22E61EAF00625807 /* EmojiPickerViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EmojiPickerViewCell.xib; sourceTree = "<group>"; };
B1DCC62F22E7026F00625807 /* EmojiPickerHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerHeaderView.swift; sourceTree = "<group>"; };
B1DCC63022E7026F00625807 /* EmojiPickerHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EmojiPickerHeaderView.xib; sourceTree = "<group>"; };
B1DCC63322E72C1B00625807 /* UISearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UISearchBar.swift; sourceTree = "<group>"; };
B1DCC63622E8541700625807 /* EmojiStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiStore.swift; sourceTree = "<group>"; };
B1DCC63822E85E9A00625807 /* EmojiMartStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartStore.swift; sourceTree = "<group>"; };
B1DCC63A22E85EF800625807 /* EmojiMartCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartCategory.swift; sourceTree = "<group>"; };
B1DCC63E22E9A3AE00625807 /* EmojiItem+EmojiMart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiItem+EmojiMart.swift"; sourceTree = "<group>"; };
B1E5368821FB1E20001F3AFF /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
B1E5368C21FB7245001F3AFF /* KeyBackupRecoverFromPassphraseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromPassphraseViewController.swift; sourceTree = "<group>"; };
B1E5368E21FB7258001F3AFF /* KeyBackupRecoverFromPassphraseViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = KeyBackupRecoverFromPassphraseViewController.storyboard; sourceTree = "<group>"; };
B1FDF55F21F5FE5500BA3834 /* KeyBackupSetupPassphraseViewAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupSetupPassphraseViewAction.swift; sourceTree = "<group>"; };
B43DC75D1590BB8A4243BD4D /* Pods-RiotPods-Riot.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotPods-Riot.release.xcconfig"; path = "Target Support Files/Pods-RiotPods-Riot/Pods-RiotPods-Riot.release.xcconfig"; sourceTree = "<group>"; };
BABB6681FBD79219B1213D6C /* Pods-RiotTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotTests.debug.xcconfig"; path = "Target Support Files/Pods-RiotTests/Pods-RiotTests.debug.xcconfig"; sourceTree = "<group>"; };
E2599D0ECB8DD206624E450B /* Pods-RiotPods-SiriIntents.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotPods-SiriIntents.release.xcconfig"; path = "Target Support Files/Pods-RiotPods-SiriIntents/Pods-RiotPods-SiriIntents.release.xcconfig"; sourceTree = "<group>"; };
F05927C71FDED835009F2A68 /* MXGroup+Riot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MXGroup+Riot.m"; sourceTree = "<group>"; };
F05927C81FDED835009F2A68 /* MXGroup+Riot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MXGroup+Riot.h"; sourceTree = "<group>"; };
@ -1317,6 +1413,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3AF393339D2D566CE14AC200 /* Pods_RiotTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1601,6 +1698,8 @@
4FC6A5D63FAD1B27C2F57AFA /* Pods-RiotPods-RiotShareExtension.release.xcconfig */,
3942DD65EBEB7AE647C6392A /* Pods-RiotPods-SiriIntents.debug.xcconfig */,
E2599D0ECB8DD206624E450B /* Pods-RiotPods-SiriIntents.release.xcconfig */,
BABB6681FBD79219B1213D6C /* Pods-RiotTests.debug.xcconfig */,
AC34BF67FD21A9D01C16AE5D /* Pods-RiotTests.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -1612,6 +1711,7 @@
1ACF09217ADF1D7E7A35BC02 /* Pods_RiotPods_Riot.framework */,
97151D7F0F892081250D50A3 /* Pods_RiotPods_RiotShareExtension.framework */,
51187E952D5CECF6D6F5A28E /* Pods_RiotPods_SiriIntents.framework */,
129EB7E27E7E4AC3F5F098F5 /* Pods_RiotTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -1762,6 +1862,39 @@
path = RecoveryKey;
sourceTree = "<group>";
};
B152C72922DCEA670041315A /* EmojiPicker */ = {
isa = PBXGroup;
children = (
B1DCC61F22E5EDA400625807 /* EmojiPickerCoordinatorBridgePresenter.swift */,
B1DCC61322E5E16D00625807 /* EmojiPickerCoordinator.swift */,
B1DCC61122E5E16A00625807 /* EmojiPickerCoordinatorType.swift */,
B1DCC61422E5E16E00625807 /* EmojiPickerViewAction.swift */,
B1DCC61222E5E16C00625807 /* EmojiPickerViewController.swift */,
B1DCC60F22E5E16900625807 /* EmojiPickerViewController.storyboard */,
B1DCC61622E5E17000625807 /* EmojiPickerViewModel.swift */,
B1DCC61522E5E16F00625807 /* EmojiPickerViewModelType.swift */,
B1DCC61022E5E16900625807 /* EmojiPickerViewState.swift */,
B1DCC62322E60CA800625807 /* EmojiPickerCategoryViewData.swift */,
B1DCC62122E60BE000625807 /* EmojiPickerItemViewData.swift */,
B1DCC62F22E7026F00625807 /* EmojiPickerHeaderView.swift */,
B1DCC63022E7026F00625807 /* EmojiPickerHeaderView.xib */,
B1DCC62B22E61EAF00625807 /* EmojiPickerViewCell.swift */,
B1DCC62C22E61EAF00625807 /* EmojiPickerViewCell.xib */,
B152C72C22DF21880041315A /* Data */,
);
path = EmojiPicker;
sourceTree = "<group>";
};
B152C72C22DF21880041315A /* Data */ = {
isa = PBXGroup;
children = (
B1B9DEDD22E9D9890065E677 /* EmojiServiceType.swift */,
B1DCC64022E9B37400625807 /* Store */,
B1B9DED722E9B56C0065E677 /* EmojiMart */,
);
path = Data;
sourceTree = "<group>";
};
B1664BAB20F4E67500808783 /* Modules */ = {
isa = PBXGroup;
children = (
@ -2064,6 +2197,7 @@
B1B556DC20EE6C4C00210D55 /* Call */,
B1B5568720EE6C4C00210D55 /* Contacts */,
B1B556ED20EE6C4C00210D55 /* MediaPicker */,
B1C3361A22F328AE0021BA8D /* Camera */,
B1B556E420EE6C4C00210D55 /* UserDevices */,
B1B5596B20EFA85C00210D55 /* EncryptionInfo */,
B1B556FD20EE6C4C00210D55 /* RoomKeyRequest */,
@ -2158,6 +2292,8 @@
B1B5569020EE6C4C00210D55 /* Settings */,
B1C562D7228C0B4C0037F12A /* ContextualMenu */,
B1963B24228F1C4800CBA17F /* BubbleReactions */,
B152C72922DCEA670041315A /* EmojiPicker */,
B1B9DEDF22EB34ED0065E677 /* ReactionHistory */,
);
path = Room;
sourceTree = "<group>";
@ -2445,11 +2581,15 @@
B1B556ED20EE6C4C00210D55 /* MediaPicker */ = {
isa = PBXGroup;
children = (
B1C335FF22F1ED5F0021BA8D /* MediaPickerCoordinatorBridgePresenter.swift */,
B1C335FE22F1ED5F0021BA8D /* MediaPickerCoordinatorType.swift */,
B1C3360022F1ED600021BA8D /* MediaPickerCoordinator.swift */,
B1B556F320EE6C4C00210D55 /* MediaPickerViewController.h */,
B1B556EF20EE6C4C00210D55 /* MediaPickerViewController.m */,
B1B556F220EE6C4C00210D55 /* MediaPickerViewController.xib */,
B1B557CD20EF5E3500210D55 /* Views */,
B1B5577720EE724200210D55 /* Library */,
B1B557CD20EF5E3500210D55 /* Views */,
B1C3361B22F32B4A0021BA8D /* SingleImagePickerPresenter.swift */,
);
path = MediaPicker;
sourceTree = "<group>";
@ -3088,6 +3228,7 @@
B1B5597C20EFC3DF00210D55 /* Managers */ = {
isa = PBXGroup;
children = (
B1B9DED822E9B7120065E677 /* Serialization */,
B1FDF56321F68C0700BA3834 /* PasswordStrength */,
B1798300211B137B001FD722 /* OnBoarding */,
B1B5598B20EFC5E400210D55 /* Analytics */,
@ -3141,6 +3282,53 @@
path = Analytics;
sourceTree = "<group>";
};
B1B9DED722E9B56C0065E677 /* EmojiMart */ = {
isa = PBXGroup;
children = (
B1DCC62922E60D1000625807 /* EmojiMartService.swift */,
B1DCC63822E85E9A00625807 /* EmojiMartStore.swift */,
B1DCC63A22E85EF800625807 /* EmojiMartCategory.swift */,
B1DCC63E22E9A3AE00625807 /* EmojiItem+EmojiMart.swift */,
);
path = EmojiMart;
sourceTree = "<group>";
};
B1B9DED822E9B7120065E677 /* Serialization */ = {
isa = PBXGroup;
children = (
B1B9DEDB22E9B7440065E677 /* SerializationServiceType.swift */,
B1B9DED922E9B7350065E677 /* SerializationService.swift */,
);
path = Serialization;
sourceTree = "<group>";
};
B1B9DEDF22EB34ED0065E677 /* ReactionHistory */ = {
isa = PBXGroup;
children = (
B120863622EF375F001F89E0 /* ReactionHistoryBridgeCoordinatorPresenter.swift */,
B1B9DEE022EB34ED0065E677 /* ReactionHistoryCoordinatorType.swift */,
B1B9DEE522EB34EE0065E677 /* ReactionHistoryCoordinator.swift */,
B1B9DEE122EB34EE0065E677 /* ReactionHistoryViewController.swift */,
B1B9DEE222EB34EE0065E677 /* ReactionHistoryViewController.storyboard */,
B1B9DEE422EB34EE0065E677 /* ReactionHistoryViewModelType.swift */,
B1B9DEE322EB34EE0065E677 /* ReactionHistoryViewModel.swift */,
B1B9DEE622EB34EE0065E677 /* ReactionHistoryViewAction.swift */,
B1B9DEE722EB34EE0065E677 /* ReactionHistoryViewState.swift */,
B1B9DEF022EB396B0065E677 /* ReactionHistoryViewData.swift */,
B1B9DEF222EB426D0065E677 /* ReactionHistoryViewCell.swift */,
B1B9DEF322EB426D0065E677 /* ReactionHistoryViewCell.xib */,
);
path = ReactionHistory;
sourceTree = "<group>";
};
B1C3361A22F328AE0021BA8D /* Camera */ = {
isa = PBXGroup;
children = (
B1C335CC22F1C1320021BA8D /* CameraPresenter.swift */,
);
path = Camera;
sourceTree = "<group>";
};
B1C562D7228C0B4C0037F12A /* ContextualMenu */ = {
isa = PBXGroup;
children = (
@ -3183,6 +3371,16 @@
path = KeyboardAvoiding;
sourceTree = "<group>";
};
B1DCC64022E9B37400625807 /* Store */ = {
isa = PBXGroup;
children = (
B1DCC63622E8541700625807 /* EmojiStore.swift */,
B1DCC62722E60CE300625807 /* EmojiCategory.swift */,
B1DCC62522E60CC600625807 /* EmojiItem.swift */,
);
path = Store;
sourceTree = "<group>";
};
B1E5368A21FB6FC0001F3AFF /* Passphrase */ = {
isa = PBXGroup;
children = (
@ -3224,6 +3422,7 @@
isa = PBXGroup;
children = (
F083BB041E7005FD00A9B29C /* RiotTests.m */,
B152C73022DF561E0041315A /* EmojiServiceTests.swift */,
F083BB071E70067700A9B29C /* Supporting Files */,
);
path = RiotTests;
@ -3232,6 +3431,7 @@
F083BB071E70067700A9B29C /* Supporting Files */ = {
isa = PBXGroup;
children = (
B152C72D22DF55D80041315A /* RiotTests-Bridging-Header.h */,
F083BB031E7005FD00A9B29C /* Info.plist */,
);
name = "Supporting Files";
@ -3265,6 +3465,7 @@
32935CB21F628B98006888C8 /* js */,
F083BBEF1E7009EC00A9B29C /* Images.xcassets */,
B169328620F3954A00746532 /* SharedImages.xcassets */,
B152C73422DF599B0041315A /* apple_emojis_data.json */,
B169329320F39E6200746532 /* LaunchScreen.storyboard */,
B169329520F39E6300746532 /* Main.storyboard */,
F083BC0E1E7009EC00A9B29C /* third_party_licenses.html */,
@ -3316,6 +3517,7 @@
B1C562C92289C2690037F12A /* UIGestureRecognizer.swift */,
B1C562CB228AB3510037F12A /* UIStackView.swift */,
B1B12B2822942315002CB419 /* UITouch.swift */,
B1DCC63322E72C1B00625807 /* UISearchBar.swift */,
);
path = Categories;
sourceTree = "<group>";
@ -3451,6 +3653,7 @@
isa = PBXNativeTarget;
buildConfigurationList = F094A9CB1B78D8F000B1FBBF /* Build configuration list for PBXNativeTarget "RiotTests" */;
buildPhases = (
CB374E980E51D4490F17AD40 /* [CP] Check Pods Manifest.lock */,
F094A9BA1B78D8F000B1FBBF /* Sources */,
F094A9BB1B78D8F000B1FBBF /* Frameworks */,
F094A9BC1B78D8F000B1FBBF /* Resources */,
@ -3515,6 +3718,7 @@
F094A9BD1B78D8F000B1FBBF = {
CreatedOnToolsVersion = 6.2;
DevelopmentTeam = 7J4U792NQT;
LastSwiftMigration = 1020;
TestTargetID = F094A9A11B78D8F000B1FBBF;
};
};
@ -3585,6 +3789,7 @@
B1B5590220EF768F00210D55 /* RoomOutgoingAttachmentWithPaginationTitleBubbleCell.xib in Resources */,
B1B558CA20EF768F00210D55 /* RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.xib in Resources */,
32891D702264DF7B00C82226 /* DeviceVerificationVerifiedViewController.storyboard in Resources */,
B152C73522DF599C0041315A /* apple_emojis_data.json in Resources */,
F083BDE91E7009ED00A9B29C /* ring.mp3 in Resources */,
B1B558F720EF768F00210D55 /* RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */,
B1B5592B20EF7A5D00210D55 /* TableViewCellWithButton.xib in Resources */,
@ -3610,6 +3815,7 @@
B1B558F220EF768F00210D55 /* RoomIncomingTextMsgWithoutSenderNameBubbleCell.xib in Resources */,
B1B558EA20EF768F00210D55 /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.xib in Resources */,
B1B558CD20EF768F00210D55 /* RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.xib in Resources */,
B1B9DEEA22EB34EF0065E677 /* ReactionHistoryViewController.storyboard in Resources */,
B1B9194C2118984300FE25B5 /* RoomPredecessorBubbleCell.xib in Resources */,
B1C562E9228C7CF20037F12A /* ContextualMenuItemView.xib in Resources */,
B1B5572120EE6C4D00210D55 /* ContactsTableViewController.xib in Resources */,
@ -3641,6 +3847,7 @@
B1B5590820EF768F00210D55 /* RoomMembershipWithPaginationTitleBubbleCell.xib in Resources */,
F083BDE81E7009ED00A9B29C /* message.mp3 in Resources */,
B1107ECA2200B09F0038014B /* KeyBackupRecoverSuccessViewController.storyboard in Resources */,
B1B9DEF522EB426D0065E677 /* ReactionHistoryViewCell.xib in Resources */,
B1B5579C20EF575B00210D55 /* ForgotPasswordInputsView.xib in Resources */,
F083BE011E7009ED00A9B29C /* third_party_licenses.html in Resources */,
B1098BFC21ECFE65000DDA48 /* PasswordStrengthView.xib in Resources */,
@ -3650,6 +3857,7 @@
3232AB2122564D9100AD6A5C /* README.md in Resources */,
B1B5593920EF7BAC00210D55 /* TableViewCellWithCheckBoxes.xib in Resources */,
B1B557C120EF5B4500210D55 /* DisabledRoomInputToolbarView.xib in Resources */,
B1DCC61722E5E17100625807 /* EmojiPickerViewController.storyboard in Resources */,
32891D6C2264CBA300C82226 /* SimpleScreenTemplateViewController.storyboard in Resources */,
B1963B2C228F1C4900CBA17F /* BubbleReactionViewCell.xib in Resources */,
B1664DA320F4F96200808783 /* Vector.strings in Resources */,
@ -3667,6 +3875,7 @@
3232AB4B2256558300AD6A5C /* TemplateScreenViewController.storyboard in Resources */,
B1D1BDA822BBAFC900831367 /* ReactionsMenuView.xib in Resources */,
32B1FEDB21A46F2C00637127 /* TermsView.xib in Resources */,
B1DCC63222E7026F00625807 /* EmojiPickerHeaderView.xib in Resources */,
B1B5578E20EF568D00210D55 /* GroupInviteTableViewCell.xib in Resources */,
B1B5582020EF625800210D55 /* SimpleRoomTitleView.xib in Resources */,
F083BE061E7009ED00A9B29C /* Riot-Defaults.plist in Resources */,
@ -3689,6 +3898,7 @@
B1098BF821ECFE65000DDA48 /* KeyBackupSetupPassphraseViewController.storyboard in Resources */,
F083BDF31E7009ED00A9B29C /* Images.xcassets in Resources */,
B1C562E4228C7C8D0037F12A /* RoomContextualMenuToolbarView.xib in Resources */,
B1DCC62E22E61EAF00625807 /* EmojiPickerViewCell.xib in Resources */,
B1B5590720EF768F00210D55 /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */,
B169329920F39E6300746532 /* LaunchScreen.storyboard in Resources */,
B1B5595320EF9A8700210D55 /* RecentTableViewCell.xib in Resources */,
@ -3839,6 +4049,28 @@
shellPath = /bin/sh;
shellScript = "${PODS_ROOT}/SwiftLint/swiftlint\n";
};
CB374E980E51D4490F17AD40 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RiotTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
FF06981FDA0DB688B8C52A41 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -3886,6 +4118,7 @@
32242F0A21E8B21300725742 /* UIColor.swift in Sources */,
32242F1921E8FBFB00725742 /* DarkTheme.swift in Sources */,
B1664BC820F4E67600808783 /* ShareDataSource.m in Sources */,
B1DCC63522E72D2200625807 /* UISearchBar.swift in Sources */,
B1664BCD20F4E67600808783 /* RecentRoomTableViewCell.m in Sources */,
B1DB4F0C2231494F0065DBFA /* String.swift in Sources */,
B169331720F3CBE000746532 /* RecentCellData.m in Sources */,
@ -3917,6 +4150,7 @@
B16932A520F3A21C00746532 /* empty.mm in Sources */,
3232AB4A2256558300AD6A5C /* FlowTemplateCoordinator.swift in Sources */,
B19EFA3B21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift in Sources */,
B1B9DEDE22E9D9890065E677 /* EmojiServiceType.swift in Sources */,
3232ABA9225730E100AD6A5C /* DeviceVerificationStartViewModel.swift in Sources */,
B16932FA20F3C51A00746532 /* RecentCellData.m in Sources */,
B16932F220F3C49E00746532 /* GroupsDataSource.m in Sources */,
@ -3926,6 +4160,8 @@
B1B5598820EFC3E000210D55 /* WidgetManager.m in Sources */,
B1DB4F0E22316FFF0065DBFA /* UserNameColorGenerator.swift in Sources */,
B1057789221304EC00334B1E /* KeyBackupSetupSuccessFromPassphraseViewController.swift in Sources */,
B1DCC61922E5E17100625807 /* EmojiPickerCoordinatorType.swift in Sources */,
B1C3360122F1ED600021BA8D /* MediaPickerCoordinatorType.swift in Sources */,
3232ABB72257BE6400AD6A5C /* DeviceVerificationVerifyViewModelType.swift in Sources */,
32F6B96D2270623100BBA352 /* DeviceVerificationDataLoadingViewModel.swift in Sources */,
B16932B120F3AC9200746532 /* RoomSearchDataSource.m in Sources */,
@ -3953,14 +4189,18 @@
B1B558F420EF768F00210D55 /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */,
B1B5572320EE6C4D00210D55 /* AttachmentsViewController.m in Sources */,
F083BDEE1E7009ED00A9B29C /* MXRoom+Riot.m in Sources */,
B120863722EF375F001F89E0 /* ReactionHistoryBridgeCoordinatorPresenter.swift in Sources */,
B1B5598620EFC3E000210D55 /* RiotSettings.swift in Sources */,
3232ABA3225730E100AD6A5C /* DeviceVerificationStartCoordinatorType.swift in Sources */,
3232AB4D2256558300AD6A5C /* TemplateScreenCoordinatorType.swift in Sources */,
B1B5581720EF625800210D55 /* PreviewRoomTitleView.m in Sources */,
B1DCC63F22E9A3AE00625807 /* EmojiItem+EmojiMart.swift in Sources */,
B1DCC61C22E5E17100625807 /* EmojiPickerViewAction.swift in Sources */,
B1098BDF21ECE09F000DDA48 /* Strings.swift in Sources */,
B1B558C420EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */,
3232ABC022594C0900AD6A5C /* VerifyEmojiCollectionViewCell.swift in Sources */,
B1963B2E228F1C4900CBA17F /* BubbleReactionViewData.swift in Sources */,
B1C3361C22F32B4A0021BA8D /* SingleImagePickerPresenter.swift in Sources */,
B1B5572F20EE6C4D00210D55 /* ReadReceiptsViewController.m in Sources */,
B1B558CB20EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */,
B169330B20F3CA3A00746532 /* Contact.m in Sources */,
@ -3970,15 +4210,18 @@
B14F143422144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewAction.swift in Sources */,
B1098BF621ECFE65000DDA48 /* KeyBackupSetupPassphraseCoordinator.swift in Sources */,
B1B558C320EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.m in Sources */,
B1B9DEDC22E9B7440065E677 /* SerializationServiceType.swift in Sources */,
B110872521F098F0003554A5 /* ActivityIndicatorPresenter.swift in Sources */,
32242F1521E8FBA900725742 /* DarkTheme.swift in Sources */,
B1D211E222BD193C00D939BD /* ReactionsMenuViewModel.swift in Sources */,
B140B4A621F89E7600E3F5FE /* KeyBackupSetupCoordinatorBridgePresenter.swift in Sources */,
B1B5577420EE702900210D55 /* WidgetViewController.m in Sources */,
B1DCC63122E7026F00625807 /* EmojiPickerHeaderView.swift in Sources */,
B139C21B21FE5B9200BB68EC /* KeyBackupRecoverFromPassphraseViewModel.swift in Sources */,
32A6001E22C661100042C1D9 /* EditHistoryCoordinatorBridgePresenter.swift in Sources */,
B1B5574A20EE6C4D00210D55 /* MediaPickerViewController.m in Sources */,
B1B5598520EFC3E000210D55 /* RageShakeManager.m in Sources */,
B1DCC62D22E61EAF00625807 /* EmojiPickerViewCell.swift in Sources */,
3232ABA8225730E100AD6A5C /* DeviceVerificationStartViewState.swift in Sources */,
B1B558D420EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */,
B169331420F3CAFC00746532 /* PublicRoomTableViewCell.m in Sources */,
@ -3995,11 +4238,14 @@
B1B5593B20EF7BAC00210D55 /* TableViewCellWithCheckBoxAndLabel.m in Sources */,
B1B5581A20EF625800210D55 /* ExpandedRoomTitleView.m in Sources */,
B1107EC82200B0720038014B /* KeyBackupRecoverSuccessViewController.swift in Sources */,
B1B9DEEB22EB34EF0065E677 /* ReactionHistoryViewModel.swift in Sources */,
B1963B2F228F1C4900CBA17F /* BubbleReactionViewCell.swift in Sources */,
B1B558E920EF768F00210D55 /* RoomSelectedStickerBubbleCell.m in Sources */,
B1C3360222F1ED600021BA8D /* MediaPickerCoordinatorBridgePresenter.swift in Sources */,
B1B558DF20EF768F00210D55 /* RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m in Sources */,
F083BE041E7009ED00A9B29C /* Tools.m in Sources */,
3275FD8C21A5A2C500B9C13D /* TermsView.swift in Sources */,
B1B9DEE822EB34EF0065E677 /* ReactionHistoryCoordinatorType.swift in Sources */,
B14F143122144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewState.swift in Sources */,
B1098C1121ED07E4000DDA48 /* NavigationRouterType.swift in Sources */,
B1B5573D20EE6C4D00210D55 /* WebViewViewController.m in Sources */,
@ -4028,7 +4274,9 @@
B1D4752721EE4E630067973F /* KeyboardAvoider.swift in Sources */,
B1D4752821EE4E630067973F /* KeyboardNotification.swift in Sources */,
B1D1BDA622BBAFB500831367 /* ReactionsMenuView.swift in Sources */,
B1B9DEF422EB426D0065E677 /* ReactionHistoryViewCell.swift in Sources */,
B1B5573C20EE6C4D00210D55 /* MasterTabBarController.m in Sources */,
B1DCC61B22E5E17100625807 /* EmojiPickerCoordinator.swift in Sources */,
32F6B96E2270623100BBA352 /* DeviceVerificationDataLoadingViewModelType.swift in Sources */,
B1B5592C20EF7A5D00210D55 /* TableViewCellWithButton.m in Sources */,
32242F1421E8FBA900725742 /* DefaultTheme.swift in Sources */,
@ -4040,16 +4288,20 @@
B1B5573620EE6C4D00210D55 /* GroupsViewController.m in Sources */,
3232ABB82257BE6500AD6A5C /* DeviceVerificationVerifyCoordinator.swift in Sources */,
B142317A22CCFA2000FFA96A /* EditHistoryCell.swift in Sources */,
B1DCC62622E60CC600625807 /* EmojiItem.swift in Sources */,
B1B5572A20EE6C4D00210D55 /* RoomMemberDetailsViewController.m in Sources */,
B1B5590120EF768F00210D55 /* RoomMembershipExpandedWithPaginationTitleBubbleCell.m in Sources */,
32F6B96B2270623100BBA352 /* DeviceVerificationDataLoadingViewAction.swift in Sources */,
B1B558C920EF768F00210D55 /* RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.m in Sources */,
B1B5571B20EE6C4D00210D55 /* DeactivateAccountViewController.m in Sources */,
B1DCC63922E85E9A00625807 /* EmojiMartStore.swift in Sources */,
B1B5590620EF768F00210D55 /* RoomMembershipCollapsedWithPaginationTitleBubbleCell.m in Sources */,
B139C21D21FE5BF500BB68EC /* KeyBackupRecoverFromPassphraseViewModelType.swift in Sources */,
F083BE031E7009ED00A9B29C /* EventFormatter.m in Sources */,
B1DCC62422E60CA900625807 /* EmojiPickerCategoryViewData.swift in Sources */,
324A2056225FC571004FE8B0 /* DeviceVerificationIncomingCoordinator.swift in Sources */,
B16932F720F3C50E00746532 /* RecentsDataSource.m in Sources */,
B1DCC63B22E85EF800625807 /* EmojiMartCategory.swift in Sources */,
3232AB4F2256558300AD6A5C /* TemplateScreenViewController.swift in Sources */,
B1B558FC20EF768F00210D55 /* RoomIncomingTextMsgWithPaginationTitleBubbleCell.m in Sources */,
B1B5572920EE6C4D00210D55 /* RoomFilesViewController.m in Sources */,
@ -4068,7 +4320,9 @@
3232ABAB225730E100AD6A5C /* DeviceVerificationCoordinator.swift in Sources */,
B1B5583E20EF6E7F00210D55 /* GroupRoomTableViewCell.m in Sources */,
B14F143522144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.swift in Sources */,
B1DCC61E22E5E17100625807 /* EmojiPickerViewModel.swift in Sources */,
B1B5574F20EE6C4D00210D55 /* RoomsViewController.m in Sources */,
B1B9DEDA22E9B7350065E677 /* SerializationService.swift in Sources */,
B1B5572520EE6C4D00210D55 /* RoomMessagesSearchViewController.m in Sources */,
B139C22121FE5D9D00BB68EC /* KeyBackupRecoverFromPassphraseViewState.swift in Sources */,
B1B5579120EF568D00210D55 /* GroupInviteTableViewCell.m in Sources */,
@ -4076,6 +4330,7 @@
B1B12B2922942315002CB419 /* UITouch.swift in Sources */,
B1B558CC20EF768F00210D55 /* RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.m in Sources */,
B1B5571D20EE6C4D00210D55 /* HomeViewController.m in Sources */,
B1DCC63722E8541700625807 /* EmojiStore.swift in Sources */,
3232ABA6225730E100AD6A5C /* DeviceVerificationStartViewController.swift in Sources */,
B16932EA20F3C39000746532 /* UnifiedSearchRecentsDataSource.m in Sources */,
B1B557DE20EF5FBB00210D55 /* FilesSearchTableViewCell.m in Sources */,
@ -4086,6 +4341,8 @@
B1098BFE21ECFE65000DDA48 /* KeyBackupSetupPassphraseViewModelType.swift in Sources */,
B1B558BE20EF768F00210D55 /* RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.m in Sources */,
F083BDED1E7009ED00A9B29C /* MXKRoomBubbleTableViewCell+Riot.m in Sources */,
B1B9DEEF22EB34EF0065E677 /* ReactionHistoryViewState.swift in Sources */,
B1DCC62022E5EDA400625807 /* EmojiPickerCoordinatorBridgePresenter.swift in Sources */,
B1B557A820EF5A1B00210D55 /* DeviceTableViewCell.m in Sources */,
B1B5572620EE6C4D00210D55 /* RoomFilesSearchViewController.m in Sources */,
B1B5583120EF66BA00210D55 /* RoomIdOrAliasTableViewCell.m in Sources */,
@ -4094,10 +4351,12 @@
F083BDFA1E7009ED00A9B29C /* RoomPreviewData.m in Sources */,
B1B557B420EF5AEF00210D55 /* EventDetailsView.m in Sources */,
B1B5577E20EE84BF00210D55 /* IncomingCallView.m in Sources */,
B1DCC62822E60CE300625807 /* EmojiCategory.swift in Sources */,
B1B5578F20EF568D00210D55 /* GroupTableViewCell.m in Sources */,
B1B5573220EE6C4D00210D55 /* GroupHomeViewController.m in Sources */,
B1B5595220EF9A8700210D55 /* RecentTableViewCell.m in Sources */,
32F6B96C2270623100BBA352 /* DeviceVerificationDataLoadingCoordinatorType.swift in Sources */,
B1DCC61D22E5E17100625807 /* EmojiPickerViewModelType.swift in Sources */,
B1B5574120EE6C4D00210D55 /* RecentsViewController.m in Sources */,
B1D250D82118AA0A000F4E93 /* RoomPredecessorBubbleCell.m in Sources */,
B1B5577120EE702800210D55 /* StickerPickerViewController.m in Sources */,
@ -4116,6 +4375,7 @@
B1798302211B13B3001FD722 /* OnBoardingManager.swift in Sources */,
B1B5573520EE6C4D00210D55 /* GroupDetailsViewController.m in Sources */,
B10B3B5B2201DD740072C76B /* KeyBackupBannerCell.swift in Sources */,
B1DCC61A22E5E17100625807 /* EmojiPickerViewController.swift in Sources */,
B1963B32228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift in Sources */,
32A6001722C661100042C1D9 /* EditHistoryViewController.swift in Sources */,
B1098BFA21ECFE65000DDA48 /* KeyBackupSetupPassphraseViewModel.swift in Sources */,
@ -4129,6 +4389,7 @@
324A2052225FC571004FE8B0 /* DeviceVerificationIncomingViewAction.swift in Sources */,
B105778D2213051E00334B1E /* KeyBackupSetupSuccessFromRecoveryKeyViewController.swift in Sources */,
B1B557D820EF5EA900210D55 /* RoomActivitiesView.m in Sources */,
B1B9DEE922EB34EF0065E677 /* ReactionHistoryViewController.swift in Sources */,
B1B5596620EF9E9B00210D55 /* RoomTableViewCell.m in Sources */,
B14F143322144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModel.swift in Sources */,
32A6001822C661100042C1D9 /* EditHistoryViewModel.swift in Sources */,
@ -4144,17 +4405,22 @@
B1B5575A20EE6C4D00210D55 /* UnifiedSearchViewController.m in Sources */,
3232AB492256558300AD6A5C /* FlowTemplateCoordinatorBridgePresenter.swift in Sources */,
B1B5572820EE6C4D00210D55 /* RoomViewController.m in Sources */,
B1B9DEED22EB34EF0065E677 /* ReactionHistoryCoordinator.swift in Sources */,
B1DCC62A22E60D1000625807 /* EmojiMartService.swift in Sources */,
B1B558C720EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.m in Sources */,
B1B558F020EF768F00210D55 /* RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m in Sources */,
926FA53F1F4C132000F826C2 /* MXSession+Riot.m in Sources */,
B1B5593820EF7BAC00210D55 /* TableViewCellWithLabelAndLargeTextView.m in Sources */,
B1DCC62222E60BE000625807 /* EmojiPickerItemViewData.swift in Sources */,
3232AB502256558300AD6A5C /* TemplateScreenViewState.swift in Sources */,
B1C335CD22F1C1320021BA8D /* CameraPresenter.swift in Sources */,
B1B558C820EF768F00210D55 /* RoomIncomingEncryptedAttachmentBubbleCell.m in Sources */,
B1B557C620EF5CD400210D55 /* DirectoryServerDetailTableViewCell.m in Sources */,
B1B5590920EF768F00210D55 /* RoomEmptyBubbleCell.m in Sources */,
324A2054225FC571004FE8B0 /* DeviceVerificationIncomingCoordinatorType.swift in Sources */,
3232ABB92257BE6500AD6A5C /* DeviceVerificationVerifyViewController.swift in Sources */,
B139C21F21FE5D6600BB68EC /* KeyBackupRecoverFromPassphraseViewAction.swift in Sources */,
B1DCC61822E5E17100625807 /* EmojiPickerViewState.swift in Sources */,
B1C562DB228C0BB00037F12A /* RoomContextualMenuAction.swift in Sources */,
B1B5574720EE6C4D00210D55 /* UsersDevicesViewController.m in Sources */,
B1098BFF21ECFE65000DDA48 /* PasswordStrengthView.swift in Sources */,
@ -4174,6 +4440,7 @@
B14F143222144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyCoordinator.swift in Sources */,
B110872621F098F0003554A5 /* ActivityIndicatorView.swift in Sources */,
B19EFA3921F8BB2C00FC070E /* KeyBackupRecoverCoordinatorType.swift in Sources */,
B1C3360322F1ED600021BA8D /* MediaPickerCoordinator.swift in Sources */,
B1E5368D21FB7245001F3AFF /* KeyBackupRecoverFromPassphraseViewController.swift in Sources */,
B1963B3822933BC800CBA17F /* AutosizedCollectionView.swift in Sources */,
B169330320F3C98900746532 /* RoomBubbleCellData.m in Sources */,
@ -4214,6 +4481,7 @@
B1098BFD21ECFE65000DDA48 /* PasswordStrengthManager.swift in Sources */,
B1B558F520EF768F00210D55 /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.m in Sources */,
3232AB482256558300AD6A5C /* FlowTemplateCoordinatorType.swift in Sources */,
B1B9DEF122EB396B0065E677 /* ReactionHistoryViewData.swift in Sources */,
B1B558F820EF768F00210D55 /* RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */,
32242F0921E8B05F00725742 /* UIColor.swift in Sources */,
B16932E720F3C37100746532 /* HomeMessagesSearchDataSource.m in Sources */,
@ -4225,10 +4493,13 @@
B1098C0021ECFE65000DDA48 /* KeyBackupSetupPassphraseViewController.swift in Sources */,
B1B5591020EF782800210D55 /* TableViewCellWithPhoneNumberTextField.m in Sources */,
B1DB4F06223015080065DBFA /* Character.swift in Sources */,
B1B9DEEE22EB34EF0065E677 /* ReactionHistoryViewAction.swift in Sources */,
32B94DFA228EC26400716A26 /* ReactionsMenuButton.swift in Sources */,
B1B9DEEC22EB34EF0065E677 /* ReactionHistoryViewModelType.swift in Sources */,
B1C562E8228C7CF20037F12A /* ContextualMenuItemView.swift in Sources */,
B14F143022144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyCoordinatorType.swift in Sources */,
B1E5368921FB1E20001F3AFF /* UIButton.swift in Sources */,
B1DCC63422E72C1B00625807 /* UISearchBar.swift in Sources */,
32A6001622C661100042C1D9 /* EditHistoryViewState.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -4238,6 +4509,7 @@
buildActionMask = 2147483647;
files = (
F083BEA51E70356E00A9B29C /* RiotTests.m in Sources */,
B152C73122DF561E0041315A /* EmojiServiceTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -4664,9 +4936,11 @@
};
F094A9CC1B78D8F000B1FBBF /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BABB6681FBD79219B1213D6C /* Pods-RiotTests.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_MODULES = YES;
DEVELOPMENT_TEAM = 7J4U792NQT;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
@ -4684,15 +4958,20 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "org.matrix.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "RiotTests/RiotTests-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Riot.app/Riot";
};
name = Debug;
};
F094A9CD1B78D8F000B1FBBF /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AC34BF67FD21A9D01C16AE5D /* Pods-RiotTests.release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_MODULES = YES;
DEVELOPMENT_TEAM = 7J4U792NQT;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
@ -4706,6 +4985,8 @@
);
PRODUCT_BUNDLE_IDENTIFIER = "org.matrix.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "RiotTests/RiotTests-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Riot.app/Riot";
};
name = Release;

View file

@ -2770,7 +2770,10 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
// Remove inApp notifications toggle change
MXKAccount *account = notif.object;
[account removeObserver:self forKeyPath:@"enableInAppNotifications"];
if (!account.isSoftLogout)
{
[account removeObserver:self forKeyPath:@"enableInAppNotifications"];
}
// Clear Modular data
[[WidgetManager sharedManager] deleteDataForUser:account.mxCredentials.userId];
@ -2781,6 +2784,16 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
[self logoutWithConfirmation:NO completion:nil];
}
}];
// Add observer to handle soft logout
[[NSNotificationCenter defaultCenter] addObserverForName:kMXKAccountManagerDidSoftlogoutAccountNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXKAccount *account = notif.object;
[self removeMatrixSession:account.mxSession];
// Return to authentication screen
[self.masterTabBarController showAuthenticationScreenAfterSoftLogout:account.mxCredentials];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionIgnoredUsersDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull notif) {

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "more_reactions.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "more_reactions@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "more_reactions@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -33,6 +33,7 @@
<key>integrationsWidgetsUrls</key>
<array>
<string>https://scalar-staging.riot.im/scalar/api</string>
<string>https://scalar-staging.vector.im/api</string>
<string>https://scalar.vector.im/api</string>
</array>
<key>piwik</key>

File diff suppressed because one or more lines are too long

View file

@ -788,3 +788,35 @@
// MARK: File upload
"file_upload_error_title" = "Качване на файл";
"file_upload_error_unsupported_file_type_message" = "Типът файл не се поддържа.";
"auth_softlogout_signed_out" = "Излезли сте от профила";
"auth_softlogout_sign_in" = "Влез";
"auth_softlogout_reason" = "Администратора на вашия сървър (%1$@) ви е отписал от профила %2$@ (%3$@).";
"auth_softlogout_recover_encryption_keys" = "Влезте за да възстановите ключове за шифровани съхранени само на това устройство. Ще ви трябват за да можете да четете всички защитени съобщения на кое да е устройство.";
"auth_softlogout_clear_data" = "Изчисти личните данни";
"auth_softlogout_clear_data_message_1" = "Внимание: Личните ви данни (включително ключове за шифроване) все още са съхранени на това устройство.";
"auth_softlogout_clear_data_message_2" = "Изчистете, ако сте приключили с използването на това устройство или искате да влезете с друг профил.";
"auth_softlogout_clear_data_button" = "Изчисти всички данни";
"auth_softlogout_clear_data_sign_out_title" = "Сигурни ли сте?";
"auth_softlogout_clear_data_sign_out_msg" = "Сигурни ли сте, че искате да изчистите всички данни съхранени на това устройство? Влезте пак за да достъпите профила и съобщенията си.";
"auth_softlogout_clear_data_sign_out" = "Излез";
"room_event_action_reaction_history" = "История на реакциите";
// MARK: Emoji picker
"emoji_picker_title" = "Реакции";
"emoji_picker_people_category" = "Усмивки и хора";
"emoji_picker_nature_category" = "Животни и природа";
"emoji_picker_foods_category" = "Храна и напитки";
"emoji_picker_activity_category" = "Дейности";
"emoji_picker_places_category" = "Пътуване и места";
"emoji_picker_objects_category" = "Обекти";
"emoji_picker_symbols_category" = "Символи";
"emoji_picker_flags_category" = "Флагове";
// MARK: Reaction history
"reaction_history_title" = "Реакции";
"room_action_camera" = "Направи снимка или видео";
// Media picker
"media_picker_title" = "Медийна библиотека";
// Image picker
"image_picker_action_camera" = "Направи снимка";
"image_picker_action_library" = "Избери от библиотеката";
"camera_unavailable" = "Не е достъпна камера на вашето устройство";
"photo_library_access_not_granted" = "%@ няма достъп до библиотеката със снимки. Моля, променете настройките на поверителността";

View file

@ -669,7 +669,7 @@
"room_action_reply" = "Antworten";
"settings_labs_message_reaction" = "Mit einem Emoji reagieren";
"settings_key_backup_button_connect" = "Schlüssel dieses Geräts sichern";
"event_formatter_message_edited_mention" = "(geändert)";
"event_formatter_message_edited_mention" = "(bearbeitet)";
"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Schlüssel dieses Geräts sichern";
"key_backup_recover_connent_banner_subtitle" = "Schlüssel dieses Geräts sichern";
// MARK: - Device Verification
@ -736,3 +736,65 @@
"device_verification_emoji_bell" = "Glocke";
"device_verification_emoji_anchor" = "Anker";
"device_verification_emoji_headphones" = "Kopfhörer";
"close" = "Schließen";
"auth_softlogout_signed_out" = "Sie sind abgemeldet";
"auth_softlogout_sign_in" = "Einloggen";
"auth_softlogout_reason" = "Ihr Homeserver-Administrator (%1$@) hat Sie von Ihrem Konto %2$@ (%3$@) abgemeldet.";
"auth_softlogout_recover_encryption_keys" = "Melden Sie sich an, um Verschlüsselungsschlüssel wiederherzustellen, die ausschließlich auf diesem Gerät gespeichert sind. Sie benötigen sie, um alle Ihre sicheren Nachrichten auf jedem Gerät lesen zu können.";
"auth_softlogout_clear_data" = "Persönliche Daten löschen";
"auth_softlogout_clear_data_message_1" = "Warnung: Ihre persönlichen Daten (einschließlich Verschlüsselungsschlüssel) sind noch auf diesem Gerät gespeichert.";
"auth_softlogout_clear_data_message_2" = "Deaktivieren Sie es, wenn Sie dieses Gerät nicht mehr verwenden oder sich bei einem anderen Konto anmelden möchten.";
"auth_softlogout_clear_data_button" = "Lösche alle Daten";
"auth_softlogout_clear_data_sign_out_title" = "Bist du sicher?";
"auth_softlogout_clear_data_sign_out_msg" = "Möchten Sie wirklich alle derzeit auf diesem Gerät gespeicherten Daten löschen? Melden Sie sich erneut an, um auf Ihre Kontodaten und Nachrichten zuzugreifen.";
"auth_softlogout_clear_data_sign_out" = "Ausloggen";
"room_event_action_reaction_show_all" = "Zeige alles";
"room_event_action_reaction_show_less" = "Zeige weniger";
"room_event_action_reaction_history" = "Reaktionsverlauf";
"room_action_send_file" = "Datei senden";
"room_message_edits_history_title" = "Bearbeitungsverlauf";
// Widget
"widget_no_integrations_server_configured" = "Kein Integrationsserver konfiguriert";
"widget_integrations_server_failed_to_connect" = "Verbindung zum Integrationsserver fehlgeschlagen";
"device_verification_security_advice" = "Für maximale Sicherheit empfehlen wir, dies persönlich zu tun oder ein anderes vertrauenswürdiges Kommunikationsmittel zu verwenden";
"device_verification_incoming_description_1" = "Überprüfen Sie dieses Gerät, um es als vertrauenswürdig zu markieren. Das Vertrauen auf Geräte von Partnern gibt Ihnen zusätzliche Sicherheit, wenn Sie verschlüsselte End-to-End-Nachrichten verwenden.";
"device_verification_incoming_description_2" = "Wenn Sie dieses Gerät überprüfen, wird es als vertrauenswürdig und für den Partner als vertrauenswürdig gekennzeichnet.";
// MARK: Start
"device_verification_start_title" = "Überprüfen Sie dies, indem Sie eine kurze Textzeichenfolge vergleichen";
"device_verification_start_wait_partner" = "Warten auf Partner zu akzeptieren ...";
"device_verification_start_use_legacy" = "Nichts auftauchend? Nicht alle Clients unterstützen die interaktive Überprüfung. Verwenden Sie die Alte-Überprüfung.";
"device_verification_start_use_legacy_action" = "Verwenden Sie die Alte-Überprüfung";
// MARK: Verify
"device_verification_verify_title_emoji" = "Überprüfen Sie dieses Gerät, indem Sie bestätigen, dass das folgende Emoji auf dem Bildschirm des Partners angezeigt wird";
"device_verification_verify_title_number" = "Überprüfen Sie dieses Gerät, indem Sie bestätigen, dass die folgenden Zahlen auf dem Bildschirm des Partners angezeigt werden";
"device_verification_verify_wait_partner" = "Warten auf die Bestätigung des Partners...";
// MARK: Verified
"device_verification_verified_title" = "Verifiziert!";
"device_verification_verified_description_1" = "Sie haben dieses Gerät erfolgreich überprüft.";
"device_verification_verified_description_2" = "Verschlüsselte Nachrichten mit diesem Benutzer werden durchgehend verschlüsselt und können von Dritten nicht gelesen werden.";
"device_verification_emoji_rooster" = "Hahn";
"device_verification_emoji_globe" = "Globus";
"device_verification_emoji_smiley" = "Lächeln";
"device_verification_emoji_spanner" = "Spanner";
"device_verification_emoji_thumbs up" = "Daumen hoch";
"device_verification_emoji_hourglass" = "Sanduhr";
"device_verification_emoji_clock" = "Uhr";
"device_verification_emoji_pencil" = "Bleistift";
"device_verification_emoji_lock" = "sperren";
"device_verification_emoji_folder" = "Ordner";
"device_verification_emoji_pin" = "Stift";
// MARK: File upload
"file_upload_error_title" = "Datei hochladen";
"file_upload_error_unsupported_file_type_message" = "Dateityp wird nicht unterstützt.";
// MARK: Emoji picker
"emoji_picker_title" = "Reaktionen";
"emoji_picker_people_category" = "Smileys & Menschen";
"emoji_picker_nature_category" = "Tiere & Natur";
"emoji_picker_foods_category" = "Essen und Trinken";
"emoji_picker_activity_category" = "Aktivitäten";
"emoji_picker_places_category" = "Reisen & Orte";
"emoji_picker_objects_category" = "Objekte";
"emoji_picker_symbols_category" = "Symbole";
"emoji_picker_flags_category" = "Flaggen";
// MARK: Reaction history
"reaction_history_title" = "Reaktionen";

View file

@ -115,6 +115,18 @@
"auth_add_email_and_phone_warning" = "Registration with email and phone number at once is not supported yet until the api exists. Only the phone number will be taken into account. You may add your email to your profile in settings.";
"auth_accept_policies" = "Please review and accept the policies of this homeserver:";
"auth_autodiscover_invalid_response" = "Invalid homeserver discovery response";
"auth_softlogout_signed_out" = "Youre signed out";
"auth_softlogout_sign_in" = "Sign In";
"auth_softlogout_reason" = "Your homeserver (%1$@) admin has signed you out of your account %2$@ (%3$@).";
"auth_softlogout_recover_encryption_keys" = "Sign in to recover encryption keys stored exclusively on this device. You need them to read all of your secure messages on any device.";
"auth_softlogout_clear_data" = "Clear personal data";
"auth_softlogout_clear_data_message_1" = "Warning: Your personal data (including encryption keys) is still stored on this device.";
"auth_softlogout_clear_data_message_2" = "Clear it if you're finished using this device, or want to sign in to another account.";
"auth_softlogout_clear_data_button" = "Clear all data";
"auth_softlogout_clear_data_sign_out_title" = "Are you sure?";
"auth_softlogout_clear_data_sign_out_msg" = "Are you sure you want to clear all data currently stored on this device? Sign in again to access your account data and messages.";
"auth_softlogout_clear_data_sign_out" = "Sign out";
// Chat creation
"room_creation_title" = "New Chat";
@ -283,8 +295,10 @@
"room_event_action_edit" = "Edit";
"room_event_action_reaction_show_all" = "Show all";
"room_event_action_reaction_show_less" = "Show less";
"room_event_action_reaction_history" = "Reaction history";
"room_warning_about_encryption" = "End-to-end encryption is in beta and may not be reliable.\n\nYou should not yet trust it to secure data.\n\nDevices will not yet be able to decrypt history from before they joined the room.\n\nEncrypted messages will not be visible on clients that do not yet implement encryption.";
"room_event_failed_to_send" = "Failed to send";
"room_action_camera" = "Take photo or video";
"room_action_send_photo_or_video" = "Send photo or video";
"room_action_send_sticker" = "Send sticker";
"room_action_send_file" = "Send file";
@ -561,9 +575,14 @@
"receipt_status_read" = "Read: ";
// Media picker
"media_picker_title" = "Media library";
"media_picker_library" = "Library";
"media_picker_select" = "Select";
// Image picker
"image_picker_action_camera" = "Take photo";
"image_picker_action_library" = "Choose from library";
// Directory
"directory_title" = "Directory";
"directory_server_picker_title" = "Select a directory";
@ -594,6 +613,8 @@
"rage_shake_prompt" = "You seem to be shaking the phone in frustration. Would you like to submit a bug report?";
"do_not_ask_again" = "Do not ask again";
"camera_access_not_granted" = "%@ doesn't have permission to use Camera, please change privacy settings";
"camera_unavailable" = "The camera is unavailable on your device";
"photo_library_access_not_granted" = "%@ doesn't have permission to access photo library, please change privacy settings";
"large_badge_value_k_format" = "%.1fK";
"room_does_not_exist" = "%@ does not exist";
@ -904,3 +925,18 @@
// MARK: File upload
"file_upload_error_title" = "File upload";
"file_upload_error_unsupported_file_type_message" = "File type not supported.";
// MARK: Emoji picker
"emoji_picker_title" = "Reactions";
"emoji_picker_people_category" = "Smileys & People";
"emoji_picker_nature_category" = "Animals & Nature";
"emoji_picker_foods_category" = "Food & Drink";
"emoji_picker_activity_category" = "Activities";
"emoji_picker_places_category" = "Travel & Places";
"emoji_picker_objects_category" = "Objects";
"emoji_picker_symbols_category" = "Symbols";
"emoji_picker_flags_category" = "Flags";
// MARK: Reaction history
"reaction_history_title" = "Reactions";

View file

@ -740,7 +740,7 @@
"device_verification_cancelled" = "Beste aldeak egiaztaketa ezeztatu du.";
"device_verification_cancelled_by_me" = "Egiaztaketa ezeztatu da. Arrazoia: %@";
"device_verification_emoji_hourglass" = "Harea-erlojua";
"device_verification_emoji_clock" = "Klasea";
"device_verification_emoji_clock" = "Erlojua";
"device_verification_emoji_gift" = "Oparia";
"device_verification_emoji_light bulb" = "Bonbilla";
"device_verification_emoji_book" = "Liburua";
@ -777,3 +777,24 @@
// MARK: File upload
"file_upload_error_title" = "Fitxategi igoera";
"file_upload_error_unsupported_file_type_message" = "Fitxategi mota ez onartua.";
"auth_softlogout_signed_out" = "Saioa amaitu duzu";
"auth_softlogout_sign_in" = "Hasi saioa";
"auth_softlogout_reason" = "Zure hasiera zerbitzariaren administratzaileak (%1$@) zure %2$@ kontuaren saioa amaitu du (%3$@).";
"auth_softlogout_recover_encryption_keys" = "hasi saioa gailu honetan besterik gorde ez diren zifratze gakoak berreskuratzeko. Zure mezu seguruak beste gailuetan irakurri ahal izateko behar dituzu.";
"auth_softlogout_clear_data" = "Garbitu datu pertsonalak";
"auth_softlogout_clear_data_message_1" = "Abisua: Zure datu pertsonalak (zure zifratze gakoak barne) gailu honetan gordeko dira.";
"auth_softlogout_clear_data_message_2" = "Garbitu gailu honekin bukatu baduzu, edo beste saio bat hasi nahi baduzu.";
"auth_softlogout_clear_data_button" = "Garbitu datu guztiak";
"auth_softlogout_clear_data_sign_out_title" = "Ziur al zaude?";
"auth_softlogout_clear_data_sign_out_msg" = "Ziur gailu honetan gordetako datu guztiak ezabatu nahi dituzula? Hasi saioa berriro zure kontuaren datuak eta mezuak atzitzeko.";
"auth_softlogout_clear_data_sign_out" = "Amaitu saioa";
// MARK: Emoji picker
"emoji_picker_title" = "Erreakzioak";
"emoji_picker_people_category" = "Irribarretxoak eta jendea";
"emoji_picker_nature_category" = "Animaliak eta natura";
"emoji_picker_foods_category" = "Janaria eta edaria";
"emoji_picker_activity_category" = "Jarduerak";
"emoji_picker_places_category" = "Bidaiak eta tokiak";
"emoji_picker_objects_category" = "Objektuak";
"emoji_picker_symbols_category" = "Sinboloak";
"emoji_picker_flags_category" = "Banderak";

View file

@ -789,3 +789,35 @@
// MARK: File upload
"file_upload_error_title" = "Envoi de fichier";
"file_upload_error_unsupported_file_type_message" = "Type de fichier non pris en charge.";
"auth_softlogout_signed_out" = "Vous êtes déconnecté(e)";
"auth_softlogout_sign_in" = "Se connecter";
"auth_softlogout_reason" = "Ladministrateur de votre serveur daccueil (%1$@) vous a déconnecté de votre compte %2$@ (%3$@).";
"auth_softlogout_recover_encryption_keys" = "Connectez-vous pour récupérer les clés de chiffrement stockées uniquement sur cet appareil. Vous en avez besoin pour lire tous les messages sécurisés sur nimporte quel appareil.";
"auth_softlogout_clear_data" = "Effacer les données personnelles";
"auth_softlogout_clear_data_message_1" = "Attention : Vos données personnelles (y compris vos clés de chiffrement) sont toujours stockées sur cet appareil.";
"auth_softlogout_clear_data_message_2" = "Effacez-les si vous nutilisez plus cet appareil ou si vous voulez vous connecter avec un autre compte.";
"auth_softlogout_clear_data_button" = "Effacer toutes les données";
"auth_softlogout_clear_data_sign_out_title" = "En êtes-vous sûr(e) ?";
"auth_softlogout_clear_data_sign_out_msg" = "Voulez vous vraiment supprimer toutes les données stockées actuellement sur cet appareil ? Reconnectez-vous pour accéder aux données et messages de votre compte.";
"auth_softlogout_clear_data_sign_out" = "Se déconnecter";
// MARK: Emoji picker
"emoji_picker_title" = "Réactions";
"emoji_picker_people_category" = "Émoticônes et personnes";
"emoji_picker_nature_category" = "Animaux et nature";
"emoji_picker_foods_category" = "Nourriture et boisson";
"emoji_picker_activity_category" = "Activités";
"emoji_picker_places_category" = "Voyage et lieux";
"emoji_picker_objects_category" = "Objets";
"emoji_picker_symbols_category" = "Symboles";
"emoji_picker_flags_category" = "Drapeaux";
"room_event_action_reaction_history" = "Historique des réactions";
// MARK: Reaction history
"reaction_history_title" = "Réactions";
"room_action_camera" = "Prendre une photo ou une vidéo";
// Media picker
"media_picker_title" = "Médiathèque";
// Image picker
"image_picker_action_camera" = "Prendre une photo";
"image_picker_action_library" = "Choisir dans la médiathèque";
"camera_unavailable" = "Lappareil photo nest pas disponible sur votre appareil";
"photo_library_access_not_granted" = "%@ na pas la permission pour accéder à la médiathèque, veuillez modifier les options de vie privée";

View file

@ -756,7 +756,7 @@
"device_verification_emoji_thumbs up" = "Hüvelykujj fel";
"device_verification_emoji_umbrella" = "Esernyő";
"device_verification_emoji_hourglass" = "Homokóra";
"device_verification_emoji_clock" = "Osztály";
"device_verification_emoji_clock" = "Óra";
"device_verification_emoji_gift" = "Ajándék";
"device_verification_emoji_light bulb" = "Égő";
"device_verification_emoji_book" = "Könyv";
@ -794,3 +794,27 @@
// MARK: File upload
"file_upload_error_title" = "Fájl feltöltés";
"file_upload_error_unsupported_file_type_message" = "A fájl típusa nem támogatott.";
"auth_softlogout_signed_out" = "Kijelentkeztél";
"auth_softlogout_sign_in" = "Bejelentkezés";
"auth_softlogout_reason" = "A matrix szerver (%1$@) adminisztrátora kiléptetett a felhasználói fiókodból %2$@ (%3$@).";
"auth_softlogout_recover_encryption_keys" = "A csak ezen az eszközön meglévő titkosítási kulcsokhoz való hozzáféréshez be kell jelentkezned. Ahhoz hogy bármelyik eszközön elolvashasd a titkosított üzeneteidet szükséged lesz rájuk.";
"auth_softlogout_clear_data" = "Személyes adatok törlése";
"auth_softlogout_clear_data_message_1" = "Figyelmeztetés: A személyes adataid (beleértve a titkosítási kulcsokat) továbbra is az eszközön tárolódnak.";
"auth_softlogout_clear_data_message_2" = "Ha már nem akarod használni ezt az eszközt vagy másik fiókba szeretnél bejelentkezni akkor töröld őket.";
"auth_softlogout_clear_data_button" = "Minden adat törlése";
"auth_softlogout_clear_data_sign_out_title" = "Biztos vagy benne?";
"auth_softlogout_clear_data_sign_out_msg" = "Biztos vagy benne, hogy minden az eszközön tárolt adatot törölni szeretnél? A fiók és az üzeneteid eléréséhez jelentkezz be.";
"auth_softlogout_clear_data_sign_out" = "Kilépés";
// MARK: Emoji picker
"emoji_picker_title" = "Reakciók";
"emoji_picker_people_category" = "Smiley-k és emberek";
"emoji_picker_nature_category" = "Állatok és természet";
"emoji_picker_foods_category" = "Étel és ital";
"emoji_picker_activity_category" = "Mozgás";
"emoji_picker_places_category" = "Utazás és helyek";
"emoji_picker_objects_category" = "Tárgyak";
"emoji_picker_symbols_category" = "Szimbólumok";
"emoji_picker_flags_category" = "Zászlók";
"room_event_action_reaction_history" = "Reakciók története";
// MARK: Reaction history
"reaction_history_title" = "Reakciók";

View file

@ -784,8 +784,48 @@
"device_verification_emoji_headphones" = "Koptelefoon";
"device_verification_emoji_folder" = "Map";
"device_verification_emoji_pin" = "Speld";
"event_formatter_message_edited_mention" = "(Bewerkt)";
"event_formatter_message_edited_mention" = "(bewerkt)";
// Widget
"widget_no_integrations_server_configured" = "Geen integratieserver geconfigureerd";
"widget_integrations_server_failed_to_connect" = "Verbinden met integratieserver mislukt";
"device_verification_emoji_lock" = "Slot";
"close" = "Sluiten";
"auth_softlogout_signed_out" = "U bent afgemeld";
"auth_softlogout_sign_in" = "Aanmelden";
"auth_softlogout_reason" = "De beheerder van uw thuisserver (%1$@) heeft u van uw account %2$@ afgemeld (%3$@).";
"auth_softlogout_recover_encryption_keys" = "Meld u aan om de versleutelingssleutels te herstellen die uitsluitend op dit apparaat worden opgeslagen. U heeft ze nodig om uw versleutelde berichten op al uw apparaten te kunnen lezen.";
"auth_softlogout_clear_data" = "Persoonlijke gegevens wissen";
"auth_softlogout_clear_data_message_1" = "Let op: uw persoonlijke gegevens (inclusief versleutelingssleutels) worden nog altijd op dit apparaat opgeslagen.";
"auth_softlogout_clear_data_message_2" = "Wis ze indien u dit apparaat niet meer gebruikt, of indien u zich wilt aanmelden met een andere account.";
"auth_softlogout_clear_data_button" = "Alle gegevens wissen";
"auth_softlogout_clear_data_sign_out_title" = "Weet u het zeker?";
"auth_softlogout_clear_data_sign_out_msg" = "Weet u zeker dat u alle gegevens die op dit moment op dit apparaat worden opgeslagen wilt wissen? Meld u opnieuw aan om toegang te verkrijgen tot uw accountgegevens en berichten.";
"auth_softlogout_clear_data_sign_out" = "Afmelden";
"room_event_action_reaction_show_all" = "Alles tonen";
"room_event_action_reaction_show_less" = "Minder tonen";
"room_event_action_reaction_history" = "Reactiegeschiedenis";
"room_action_camera" = "Foto of video maken";
"room_action_send_file" = "Bestand versturen";
"room_message_edits_history_title" = "Berichtbewerkingen";
// Media picker
"media_picker_title" = "Mediatheek";
// Image picker
"image_picker_action_camera" = "Foto maken";
"image_picker_action_library" = "Kiezen uit mediatheek";
"camera_unavailable" = "De camera is niet beschikbaar op uw apparaat";
"photo_library_access_not_granted" = "%@ heeft geen toegang tot de fotobibliotheek, wijzig uw privacy-instellingen";
// MARK: File upload
"file_upload_error_title" = "Bestand uploaden";
"file_upload_error_unsupported_file_type_message" = "Bestandstype niet ondersteund.";
// MARK: Emoji picker
"emoji_picker_title" = "Reacties";
"emoji_picker_people_category" = "Smileys en personen";
"emoji_picker_nature_category" = "Dieren en natuur";
"emoji_picker_foods_category" = "Eten en drinken";
"emoji_picker_activity_category" = "Activiteiten";
"emoji_picker_places_category" = "Reizen en plaatsen";
"emoji_picker_objects_category" = "Voorwerpen";
"emoji_picker_symbols_category" = "Symbolen";
"emoji_picker_flags_category" = "Vlaggen";
// MARK: Reaction history
"reaction_history_title" = "Reacties";

View file

@ -510,3 +510,19 @@
"settings_on_denied_notification" = "Powiadomienia są odrzucane w %@, proszę zezwól na nie w ustawieniach urządzenia";
"settings_callkit_info" = "Odbieraj połączenia przychodzące na ekranie blokady. Zobacz swoje połęczenia Riot w historii połączeń w systemie. Jeśli usługa iCloud jest włączona, historia połączeń zostanie udostępniona Apple.";
"settings_ui_theme_picker_message" = "\"Auto\" używa ustawienia \"Odwróć kolory\" urządzenia";
"close" = "Zamknij";
"auth_softlogout_sign_in" = "Zaloguj się";
"auth_softlogout_clear_data_button" = "Wyczyść wszystkie dane";
"auth_softlogout_clear_data_sign_out" = "Wyloguj się";
"room_event_action_reaction_show_all" = "Pokaż wszystko";
"room_event_action_reaction_show_less" = "Pokaż mniej";
"room_action_send_file" = "Wyślij plik";
"room_message_edits_history_title" = "Edycje wiadomości";
"settings_enable_callkit" = "Zintegrowane połączenie";
"settings_key_backup_info_checking" = "Sprawdzanie...";
"settings_key_backup_info_version" = "Wersja kopii zapasowej kluczy: %@";
"settings_key_backup_info_algorithm" = "Algorytm: %@";
"settings_key_backup_info_progress" = "Tworzenie kopii zapasowej %@ kluczy...";
"settings_key_backup_info_progress_done" = "Utworzono kopię zapasową wszystkich kluczy";
"settings_key_backup_button_delete" = "Usuń kopię zapasową";
"settings_key_backup_delete_confirmation_prompt_title" = "Usuń kopię zapasową";

View file

@ -1359,6 +1359,23 @@
href="https://www.apache.org/licenses/LICENSE-2.0">https://www.apache.org/licenses/LICENSE-2.0</a>
<br/><br/>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.
</li>
<li>
<p><b>Emoji Mart</b> (<a href="https://github.com/missive/emoji-mart">https://github.com/missive/emoji-mart</a>)</p>
<pre>
Copyright (c) 2016, Missive
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</pre>
</li>
</ul>
</body>
</html>

View file

@ -1 +1,12 @@
// String for App Store
"store_short_description" = "Veilig en gedecentraliseerd chattn en belln";
// Titles
"title_home" = "Thuus";
"title_favourites" = "Favorietn";
"title_people" = "Menschn";
"title_rooms" = "Gesprekkn";
"title_groups" = "Gemeenschappn";
"warning" = "Woarschuwienge";
// Actions
"view" = "Toogn";
"next" = "Volgende";

View file

@ -26,10 +26,22 @@ extern NSString *const kMXKRoomBubbleCellRiotEditButtonPressed;
/**
Action identifier used when the user tapped on receipts area.
The 'userInfo' disctionary contains an 'MXKReceiptSendersContainer' object under the 'kMXKRoomBubbleCellReceiptsContainerKey' key, representing the receipts container which was tapped on.
The 'userInfo' dictionary contains an 'MXKReceiptSendersContainer' object under the 'kMXKRoomBubbleCellReceiptsContainerKey' key, representing the receipts container which was tapped on.
*/
extern NSString *const kMXKRoomBubbleCellTapOnReceiptsContainer;
/**
Action identifier used when the user perform a long press on reactions view.
The 'userInfo' dictionary contains a 'NSString' object under the 'kMXKRoomBubbleCellEventIdKey' key, representing the event id of the event associated with the reactions.
*/
extern NSString *const kMXKRoomBubbleCellLongPressOnReactionView;
/**
'userInfo' dictionary key 'kMXKRoomBubbleCellEventIdKey' is associated to a 'NSString' object representing an event id.
*/
extern NSString *const kMXKRoomBubbleCellEventIdKey;
/**
Define a `MXKRoomBubbleTableViewCell` category at Riot level to handle bubble customisation.
*/

View file

@ -29,6 +29,8 @@
NSString *const kMXKRoomBubbleCellRiotEditButtonPressed = @"kMXKRoomBubbleCellRiotEditButtonPressed";
NSString *const kMXKRoomBubbleCellTapOnReceiptsContainer = @"kMXKRoomBubbleCellTapOnReceiptsContainer";
NSString *const kMXKRoomBubbleCellLongPressOnReactionView = @"kMXKRoomBubbleCellLongPressOnReactionView";
NSString *const kMXKRoomBubbleCellEventIdKey = @"kMXKRoomBubbleCellEventIdKey";
@implementation MXKRoomBubbleTableViewCell (Riot)

View file

@ -29,4 +29,13 @@ extension String {
}
return abs(hash)
}
/// Locale-independent case-insensitive contains
/// Note: Prefer use `localizedCaseInsensitiveContains` when locale matters
///
/// - Parameter other: The other string.
/// - Returns: true if current string contains other string.
func vc_caseInsensitiveContains(_ other: String) -> Bool {
return self.range(of: other, options: .caseInsensitive) != nil
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
extension UISearchBar {
/// Returns internal UITextField
@objc var vc_searchTextField: UITextField? {
// TODO: To remove once on XCode11/iOS13
#if swift(>=5.1)
if #available(iOS 13.0, *) {
return self.searchTextField
} else {
return self.value(forKey: "searchField") as? UITextField
}
#else
return self.value(forKey: "searchField") as? UITextField
#endif
}
}

View file

@ -89,6 +89,7 @@ internal enum Asset {
internal static let mainAliasIcon = ImageAsset(name: "main_alias_icon")
internal static let membersListIcon = ImageAsset(name: "members_list_icon")
internal static let modIcon = ImageAsset(name: "mod_icon")
internal static let moreReactions = ImageAsset(name: "more_reactions")
internal static let fileDocIcon = ImageAsset(name: "file_doc_icon")
internal static let fileMusicIcon = ImageAsset(name: "file_music_icon")
internal static let filePhotoIcon = ImageAsset(name: "file_photo_icon")

View file

@ -42,6 +42,11 @@ internal enum StoryboardScene {
internal static let initialScene = InitialSceneType<Riot.EditHistoryViewController>(storyboard: EditHistoryViewController.self)
}
internal enum EmojiPickerViewController: StoryboardType {
internal static let storyboardName = "EmojiPickerViewController"
internal static let initialScene = InitialSceneType<Riot.EmojiPickerViewController>(storyboard: EmojiPickerViewController.self)
}
internal enum KeyBackupRecoverFromPassphraseViewController: StoryboardType {
internal static let storyboardName = "KeyBackupRecoverFromPassphraseViewController"
@ -77,6 +82,11 @@ internal enum StoryboardScene {
internal static let initialScene = InitialSceneType<Riot.KeyBackupSetupSuccessFromRecoveryKeyViewController>(storyboard: KeyBackupSetupSuccessFromRecoveryKeyViewController.self)
}
internal enum ReactionHistoryViewController: StoryboardType {
internal static let storyboardName = "ReactionHistoryViewController"
internal static let initialScene = InitialSceneType<Riot.ReactionHistoryViewController>(storyboard: ReactionHistoryViewController.self)
}
internal enum RoomContextualMenuViewController: StoryboardType {
internal static let storyboardName = "RoomContextualMenuViewController"

View file

@ -226,6 +226,50 @@ internal enum VectorL10n {
internal static var authSkip: String {
return VectorL10n.tr("Vector", "auth_skip")
}
/// Clear personal data
internal static var authSoftlogoutClearData: String {
return VectorL10n.tr("Vector", "auth_softlogout_clear_data")
}
/// Clear all data
internal static var authSoftlogoutClearDataButton: String {
return VectorL10n.tr("Vector", "auth_softlogout_clear_data_button")
}
/// Warning: Your personal data (including encryption keys) is still stored on this device.
internal static var authSoftlogoutClearDataMessage1: String {
return VectorL10n.tr("Vector", "auth_softlogout_clear_data_message_1")
}
/// Clear it if you're finished using this device, or want to sign in to another account.
internal static var authSoftlogoutClearDataMessage2: String {
return VectorL10n.tr("Vector", "auth_softlogout_clear_data_message_2")
}
/// Sign out
internal static var authSoftlogoutClearDataSignOut: String {
return VectorL10n.tr("Vector", "auth_softlogout_clear_data_sign_out")
}
/// Are you sure you want to clear all data currently stored on this device? Sign in again to access your account data and messages.
internal static var authSoftlogoutClearDataSignOutMsg: String {
return VectorL10n.tr("Vector", "auth_softlogout_clear_data_sign_out_msg")
}
/// Are you sure?
internal static var authSoftlogoutClearDataSignOutTitle: String {
return VectorL10n.tr("Vector", "auth_softlogout_clear_data_sign_out_title")
}
/// Your homeserver (%1$@) admin has signed you out of your account %2$@ (%3$@).
internal static func authSoftlogoutReason(_ p1: String, _ p2: String, _ p3: String) -> String {
return VectorL10n.tr("Vector", "auth_softlogout_reason", p1, p2, p3)
}
/// Sign in to recover encryption keys stored exclusively on this device. You need them to read all of your secure messages on any device.
internal static var authSoftlogoutRecoverEncryptionKeys: String {
return VectorL10n.tr("Vector", "auth_softlogout_recover_encryption_keys")
}
/// Sign In
internal static var authSoftlogoutSignIn: String {
return VectorL10n.tr("Vector", "auth_softlogout_sign_in")
}
/// Youre signed out
internal static var authSoftlogoutSignedOut: String {
return VectorL10n.tr("Vector", "auth_softlogout_signed_out")
}
/// Submit
internal static var authSubmit: String {
return VectorL10n.tr("Vector", "auth_submit")
@ -330,6 +374,10 @@ internal enum VectorL10n {
internal static func cameraAccessNotGranted(_ p1: String) -> String {
return VectorL10n.tr("Vector", "camera_access_not_granted", p1)
}
/// The camera is unavailable on your device
internal static var cameraUnavailable: String {
return VectorL10n.tr("Vector", "camera_unavailable")
}
/// Cancel
internal static var cancel: String {
return VectorL10n.tr("Vector", "cancel")
@ -870,6 +918,42 @@ internal enum VectorL10n {
internal static var e2eRoomKeyRequestTitle: String {
return VectorL10n.tr("Vector", "e2e_room_key_request_title")
}
/// Activities
internal static var emojiPickerActivityCategory: String {
return VectorL10n.tr("Vector", "emoji_picker_activity_category")
}
/// Flags
internal static var emojiPickerFlagsCategory: String {
return VectorL10n.tr("Vector", "emoji_picker_flags_category")
}
/// Food & Drink
internal static var emojiPickerFoodsCategory: String {
return VectorL10n.tr("Vector", "emoji_picker_foods_category")
}
/// Animals & Nature
internal static var emojiPickerNatureCategory: String {
return VectorL10n.tr("Vector", "emoji_picker_nature_category")
}
/// Objects
internal static var emojiPickerObjectsCategory: String {
return VectorL10n.tr("Vector", "emoji_picker_objects_category")
}
/// Smileys & People
internal static var emojiPickerPeopleCategory: String {
return VectorL10n.tr("Vector", "emoji_picker_people_category")
}
/// Travel & Places
internal static var emojiPickerPlacesCategory: String {
return VectorL10n.tr("Vector", "emoji_picker_places_category")
}
/// Symbols
internal static var emojiPickerSymbolsCategory: String {
return VectorL10n.tr("Vector", "emoji_picker_symbols_category")
}
/// Reactions
internal static var emojiPickerTitle: String {
return VectorL10n.tr("Vector", "emoji_picker_title")
}
/// Send an encrypted message
internal static var encryptedRoomMessagePlaceholder: String {
return VectorL10n.tr("Vector", "encrypted_room_message_placeholder")
@ -1030,6 +1114,14 @@ internal enum VectorL10n {
internal static var homeserverConnectionLost: String {
return VectorL10n.tr("Vector", "homeserver_connection_lost")
}
/// Take photo
internal static var imagePickerActionCamera: String {
return VectorL10n.tr("Vector", "image_picker_action_camera")
}
/// Choose from library
internal static var imagePickerActionLibrary: String {
return VectorL10n.tr("Vector", "image_picker_action_library")
}
/// Invite
internal static var invite: String {
return VectorL10n.tr("Vector", "invite")
@ -1274,6 +1366,10 @@ internal enum VectorL10n {
internal static var mediaPickerSelect: String {
return VectorL10n.tr("Vector", "media_picker_select")
}
/// Media library
internal static var mediaPickerTitle: String {
return VectorL10n.tr("Vector", "media_picker_title")
}
/// The Internet connection appears to be offline.
internal static var networkOfflinePrompt: String {
return VectorL10n.tr("Vector", "network_offline_prompt")
@ -1314,6 +1410,10 @@ internal enum VectorL10n {
internal static var peopleNoConversation: String {
return VectorL10n.tr("Vector", "people_no_conversation")
}
/// %@ doesn't have permission to access photo library, please change privacy settings
internal static func photoLibraryAccessNotGranted(_ p1: String) -> String {
return VectorL10n.tr("Vector", "photo_library_access_not_granted", p1)
}
/// Preview
internal static var preview: String {
return VectorL10n.tr("Vector", "preview")
@ -1326,6 +1426,10 @@ internal enum VectorL10n {
internal static var rageShakePrompt: String {
return VectorL10n.tr("Vector", "rage_shake_prompt")
}
/// Reactions
internal static var reactionHistoryTitle: String {
return VectorL10n.tr("Vector", "reaction_history_title")
}
/// Read Receipts List
internal static var readReceiptsList: String {
return VectorL10n.tr("Vector", "read_receipts_list")
@ -1354,6 +1458,10 @@ internal enum VectorL10n {
internal static var retry: String {
return VectorL10n.tr("Vector", "retry")
}
/// Take photo or video
internal static var roomActionCamera: String {
return VectorL10n.tr("Vector", "room_action_camera")
}
/// Reply
internal static var roomActionReply: String {
return VectorL10n.tr("Vector", "room_action_reply")
@ -1738,6 +1846,10 @@ internal enum VectorL10n {
internal static var roomEventActionQuote: String {
return VectorL10n.tr("Vector", "room_event_action_quote")
}
/// Reaction history
internal static var roomEventActionReactionHistory: String {
return VectorL10n.tr("Vector", "room_event_action_reaction_history")
}
/// Show all
internal static var roomEventActionReactionShowAll: String {
return VectorL10n.tr("Vector", "room_event_action_reaction_show_all")

View file

@ -0,0 +1,35 @@
/*
Copyright 2019 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
final class SerializationService: SerializationServiceType {
// MARK: - Properties
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
// MARK: - Public
func deserialize<T: Decodable>(_ data: Data) throws -> T {
return try decoder.decode(T.self, from: data)
}
func serialize<T: Encodable>(_ object: T) throws -> Data {
return try encoder.encode(object)
}
}

View file

@ -0,0 +1,22 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol SerializationServiceType {
func deserialize<T: Decodable>(_ data: Data) throws -> T
func serialize<T: Encodable>(_ object: T) throws -> Data
}

View file

@ -93,7 +93,11 @@ class DarkTheme: NSObject, Theme {
func applyStyle(onSearchBar searchBar: UISearchBar) {
searchBar.barStyle = .black
searchBar.tintColor = self.searchPlaceholderColor
searchBar.barTintColor = self.headerBackgroundColor
searchBar.barTintColor = self.headerBackgroundColor
if let searchBarTextField = searchBar.vc_searchTextField {
searchBarTextField.textColor = searchBar.tintColor
}
}
func applyStyle(onTextField texField: UITextField) {

View file

@ -94,6 +94,10 @@ class DefaultTheme: NSObject, Theme {
searchBar.barStyle = .default
searchBar.tintColor = self.searchPlaceholderColor
searchBar.barTintColor = self.headerBackgroundColor
if let searchBarTextField = searchBar.vc_searchTextField {
searchBarTextField.textColor = searchBar.tintColor
}
}
func applyStyle(onTextField texField: UITextField) {

View file

@ -41,5 +41,9 @@
@property (weak, nonatomic) IBOutlet UIView *homeServerSeparator;
@property (weak, nonatomic) IBOutlet UIView *identityServerSeparator;
@property (weak, nonatomic) IBOutlet UIView *softLogoutClearDataContainer;
@property (weak, nonatomic) IBOutlet UILabel *softLogoutClearDataLabel;
@property (weak, nonatomic) IBOutlet UIButton *softLogoutClearDataButton;
@end

View file

@ -1,7 +1,8 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 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
@ -107,6 +108,14 @@
[self.customServersTickButton setImage:[UIImage imageNamed:@"selection_untick"] forState:UIControlStateHighlighted];
[self hideCustomServers:YES];
// Soft logout section
self.softLogoutClearDataButton.layer.cornerRadius = 5;
self.softLogoutClearDataButton.clipsToBounds = YES;
[self.softLogoutClearDataButton setTitle:NSLocalizedStringFromTable(@"auth_softlogout_clear_data_button", @"Vector", nil) forState:UIControlStateNormal];
[self.softLogoutClearDataButton setTitle:NSLocalizedStringFromTable(@"auth_softlogout_clear_data_button", @"Vector", nil) forState:UIControlStateHighlighted];
self.softLogoutClearDataButton.enabled = YES;
self.softLogoutClearDataContainer.hidden = YES;
// The view controller dismiss itself on successful login.
self.delegate = self;
@ -194,7 +203,10 @@
self.identityServerLabel.textColor = ThemeService.shared.theme.textSecondaryColor;
self.activityIndicator.backgroundColor = ThemeService.shared.theme.overlayBackgroundColor;
self.softLogoutClearDataLabel.textColor = ThemeService.shared.theme.textPrimaryColor;
self.softLogoutClearDataButton.backgroundColor = ThemeService.shared.theme.warningColor;
[self.authInputsView customizeViewRendering];
[self setNeedsStatusBarAppearanceUpdate];
@ -288,6 +300,7 @@
}
[self updateForgotPwdButtonVisibility];
[self updateSoftLogoutClearDataContainerVisibility];
}
- (void)setAuthInputsView:(MXKAuthInputsView *)authInputsView
@ -366,7 +379,7 @@
// The right bar button is used to switch the authentication type.
if (self.authType == MXKAuthenticationTypeLogin)
{
if (!authInputsview.isSingleSignOnRequired)
if (!authInputsview.isSingleSignOnRequired && !self.softLogoutCredentials)
{
self.rightBarButtonItem.title = NSLocalizedStringFromTable(@"auth_register", @"Vector", nil);
}
@ -395,19 +408,121 @@
}
}
- (void)setSoftLogoutCredentials:(MXCredentials *)softLogoutCredentials
{
[super setSoftLogoutCredentials:softLogoutCredentials];
// Customise the screen for soft logout
self.customServersTickButton.hidden = YES;
self.rightBarButtonItem.title = nil;
self.mainNavigationItem.title = NSLocalizedStringFromTable(@"auth_softlogout_signed_out", @"Vector", nil);
[self showSoftLogoutClearDataContainer];
}
- (void)showSoftLogoutClearDataContainer
{
NSMutableAttributedString *message = [[NSMutableAttributedString alloc] initWithString:NSLocalizedStringFromTable(@"auth_softlogout_clear_data", @"Vector", nil)
attributes:@{
NSFontAttributeName: [UIFont boldSystemFontOfSize:14]
}];
[message appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\n"]];
NSString *string = [NSString stringWithFormat:@"%@\n\n%@",
NSLocalizedStringFromTable(@"auth_softlogout_clear_data_message_1", @"Vector", nil),
NSLocalizedStringFromTable(@"auth_softlogout_clear_data_message_2", @"Vector", nil)];
[message appendAttributedString:[[NSAttributedString alloc] initWithString:string
attributes:@{
NSFontAttributeName: [UIFont systemFontOfSize:14]
}]];
self.softLogoutClearDataLabel.attributedText = message;
self.softLogoutClearDataContainer.hidden = NO;
[self refreshContentViewHeightConstraint];
}
- (void)updateSoftLogoutClearDataContainerVisibility
{
// Do not display it in case of forget password flow
if (self.softLogoutCredentials && self.authType == MXKAuthenticationTypeLogin)
{
self.softLogoutClearDataContainer.hidden = NO;
}
else
{
self.softLogoutClearDataContainer.hidden = YES;
}
}
- (void)showClearDataAfterSoftLogoutConfirmation
{
// Request confirmation
if (alert)
{
[alert dismissViewControllerAnimated:NO completion:nil];
}
alert = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"auth_softlogout_clear_data_sign_out_title", @"Vector", nil)
message:NSLocalizedStringFromTable(@"auth_softlogout_clear_data_sign_out_msg", @"Vector", nil)
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"auth_softlogout_clear_data_sign_out", @"Vector", nil) style:UIAlertActionStyleDestructive
handler:^(UIAlertAction * action)
{
[self clearDataAfterSoftLogout];
}]];
MXWeakify(self);
[alert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
MXStrongifyAndReturnIfNil(self);
self->alert = nil;
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)clearDataAfterSoftLogout
{
NSLog(@"[AuthenticationVC] clearDataAfterSoftLogout %@", self.softLogoutCredentials.userId);
// Use AppDelegate so that we reset app settings and this auth screen
[[AppDelegate theDelegate] logoutSendingRequestServer:YES completion:^(BOOL isLoggedOut) {
NSLog(@"[AuthenticationVC] Complete. isLoggedOut: %@", @(isLoggedOut));
}];
}
- (void)handleAuthenticationSession:(MXAuthenticationSession *)authSession
{
[super handleAuthenticationSession:authSession];
// Hide "Forgot password" and "Log in" buttons in case of SSO
[self updateForgotPwdButtonVisibility];
AuthInputsView *authInputsview;
if ([self.authInputsView isKindOfClass:AuthInputsView.class])
{
authInputsview = (AuthInputsView*)self.authInputsView;
}
// Hide "Forgot password" and "Log in" buttons in case of SSO
[self updateForgotPwdButtonVisibility];
[self updateSoftLogoutClearDataContainerVisibility];
self.submitButton.hidden = authInputsview.isSingleSignOnRequired;
// Bind ssoButton again if self.authInputsView has changed
[authInputsview.ssoButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
if (authInputsview.isSingleSignOnRequired && self.softLogoutCredentials)
{
// Remove submitButton so that the 2nd contraint on softLogoutClearDataContainer.top will be applied
// That makes softLogoutClearDataContainer appear upper in the screen
[self.submitButton removeFromSuperview];
}
}
- (IBAction)onButtonPressed:(id)sender
@ -529,10 +644,16 @@
[ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar];
}
else if (sender == self.softLogoutClearDataButton)
{
[self showClearDataAfterSoftLogoutConfirmation];
}
else
{
[super onButtonPressed:sender];
}
[self updateSoftLogoutClearDataContainerVisibility];
}
- (void)onFailureDuringAuthRequest:(NSError *)error
@ -540,7 +661,7 @@
// Homeserver migration: When the default homeserver url is different from matrix.org,
// the login (or forgot pwd) process with an existing matrix.org accounts will then fail.
// Patch: Falling back to matrix.org HS so we don't break everyone's logins
if ([self.homeServerTextField.text isEqualToString:self.defaultHomeServerUrl] && ![self.defaultHomeServerUrl isEqualToString:@"https://matrix.org"])
if ([self.homeServerTextField.text isEqualToString:self.defaultHomeServerUrl] && ![self.defaultHomeServerUrl isEqualToString:@"https://matrix.org"] && !self.softLogoutCredentials)
{
MXError *mxError = [[MXError alloc] initWithNSError:error];
@ -710,7 +831,13 @@
}
}
}
if (!self.softLogoutClearDataContainer.isHidden)
{
// The soft logout clear data section adds more height
constant += self.softLogoutClearDataContainer.frame.size.height;
}
self.contentViewHeightConstraint.constant = constant;
}

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina5_5" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -42,6 +42,9 @@
<outlet property="rightBarButtonItem" destination="Kwt-KN-aVL" id="Y3F-wA-tf8"/>
<outlet property="serverOptionsContainer" destination="FIn-2w-e6H" id="Z0e-NN-3LY"/>
<outlet property="skipButton" destination="wEJ-AF-rdH" id="smu-MS-2IY"/>
<outlet property="softLogoutClearDataButton" destination="ZRO-C2-hH1" id="PDa-aM-Hso"/>
<outlet property="softLogoutClearDataContainer" destination="vX2-5Y-rQc" id="mgK-41-cnX"/>
<outlet property="softLogoutClearDataLabel" destination="QYL-Lo-tmH" id="ks9-5X-xfs"/>
<outlet property="submitButton" destination="k3J-Eg-itz" id="fiZ-wK-6YM"/>
<outlet property="submitButtonMinLeadingConstraint" destination="bEB-EO-b14" id="Iz5-ks-nSX"/>
<outlet property="view" destination="5rn-KE-plm" id="bFJ-yJ-vc0"/>
@ -345,17 +348,67 @@
<constraint firstAttribute="trailing" secondItem="6yx-o1-vbD" secondAttribute="trailing" constant="19" id="rWk-Mp-KPS"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vX2-5Y-rQc">
<rect key="frame" x="0.0" y="96" width="375" height="170"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QYL-Lo-tmH">
<rect key="frame" x="10" y="0.0" width="355" height="121"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="100" id="jFf-id-CJn"/>
</constraints>
<string key="text">Clear personal data
Warning: Your personal data (including encryption keys) is still stored on this device.
Clear it if you're finished using this device, or want to sign in to another account.</string>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
<string key="userLabel">Clear personal data Warning: Your personal data (including encryption keys) is still stored on this device. Clear it if you're finished using this device, or want to sign in to another account.</string>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZRO-C2-hH1">
<rect key="frame" x="245" y="129" width="119" height="30"/>
<color key="backgroundColor" red="0.4624713659286499" green="0.81734329462051392" blue="0.47220504283905029" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="WjZ-Bh-No3"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="dvm-Bz-pT0"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
<inset key="contentEdgeInsets" minX="10" minY="0.0" maxX="10" maxY="0.0"/>
<state key="normal" title="Clear all data">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="c0b-DK-MIg"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="ZRO-C2-hH1" firstAttribute="top" secondItem="QYL-Lo-tmH" secondAttribute="bottom" constant="8" id="29j-dP-GC6"/>
<constraint firstItem="QYL-Lo-tmH" firstAttribute="leading" secondItem="vX2-5Y-rQc" secondAttribute="leading" constant="10" id="3Qr-IT-GDa"/>
<constraint firstAttribute="trailing" secondItem="ZRO-C2-hH1" secondAttribute="trailing" constant="11" id="Cj8-Kx-8vu"/>
<constraint firstAttribute="trailing" secondItem="QYL-Lo-tmH" secondAttribute="trailing" constant="10" id="JoV-ju-IhL"/>
<constraint firstItem="QYL-Lo-tmH" firstAttribute="top" secondItem="vX2-5Y-rQc" secondAttribute="top" id="Xjs-ZO-7fl"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="170" id="oUw-Jb-GkG"/>
<constraint firstAttribute="bottom" secondItem="ZRO-C2-hH1" secondAttribute="bottom" constant="11" id="q0c-yG-I0n"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<accessibility key="accessibilityConfiguration" identifier="AuthenticationVCOptionsContainer"/>
<constraints>
<constraint firstItem="FIn-2w-e6H" firstAttribute="leading" secondItem="Gg0-TE-OGb" secondAttribute="leading" id="3G8-Tb-KaN"/>
<constraint firstItem="wEJ-AF-rdH" firstAttribute="width" secondItem="k3J-Eg-itz" secondAttribute="width" id="7sB-YJ-eX4"/>
<constraint firstAttribute="trailing" secondItem="vX2-5Y-rQc" secondAttribute="trailing" id="DPh-Jx-WP1"/>
<constraint firstItem="vX2-5Y-rQc" firstAttribute="top" secondItem="Gg0-TE-OGb" secondAttribute="top" priority="100" id="QSG-jB-VcV"/>
<constraint firstItem="wEJ-AF-rdH" firstAttribute="centerY" secondItem="k3J-Eg-itz" secondAttribute="centerY" id="Vze-EI-oXj"/>
<constraint firstAttribute="trailing" secondItem="k3J-Eg-itz" secondAttribute="trailing" constant="11" id="ZyA-Tq-Sfq"/>
<constraint firstItem="k3J-Eg-itz" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Gg0-TE-OGb" secondAttribute="leading" constant="150" id="bEB-EO-b14"/>
<constraint firstItem="vX2-5Y-rQc" firstAttribute="top" secondItem="k3J-Eg-itz" secondAttribute="bottom" constant="33" id="db7-2y-vPZ"/>
<constraint firstItem="AJ2-lJ-NUq" firstAttribute="centerY" secondItem="k3J-Eg-itz" secondAttribute="centerY" id="dcE-Vs-7Rt"/>
<constraint firstAttribute="trailing" secondItem="FIn-2w-e6H" secondAttribute="trailing" id="kFj-6g-v3H"/>
<constraint firstItem="vX2-5Y-rQc" firstAttribute="leading" secondItem="Gg0-TE-OGb" secondAttribute="leading" id="kYN-Lj-zYP"/>
<constraint firstAttribute="height" constant="300" id="lXv-gM-CjN"/>
<constraint firstItem="k3J-Eg-itz" firstAttribute="top" secondItem="Gg0-TE-OGb" secondAttribute="top" constant="33" id="mor-t9-7Ke"/>
<constraint firstItem="FIn-2w-e6H" firstAttribute="top" secondItem="k3J-Eg-itz" secondAttribute="bottom" constant="5" id="oTS-5o-MMW"/>

View file

@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 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.
@ -54,6 +55,7 @@
@end
@implementation AuthInputsView
@synthesize softLogoutCredentials;
+ (UINib *)nib
{
@ -487,6 +489,15 @@
}
}
}
// For soft logout, pass the device_id currently used
if (parameters && self.softLogoutCredentials)
{
NSMutableDictionary *parametersWithDeviceId = [parameters mutableCopy];
parametersWithDeviceId[@"device_id"] = self.softLogoutCredentials.deviceId;
parameters = parametersWithDeviceId;
}
}
else if (type == MXKAuthenticationTypeRegister)
{
@ -725,7 +736,12 @@
{
// Note: this use case was not tested yet.
parameters = @{
@"auth": @{@"session":currentSession.session, @"username": self.userLoginTextField.text, @"password": self.passWordTextField.text, @"type": kMXLoginFlowTypePassword}
@"auth": @{
@"session":currentSession.session,
@"username": self.userLoginTextField.text,
@"password": self.passWordTextField.text,
@"type": kMXLoginFlowTypePassword
}
};
}
else if ([self isFlowSupported:kMXLoginFlowTypeTerms] && ![self isFlowCompleted:kMXLoginFlowTypeTerms])
@ -936,6 +952,71 @@
return YES;
}
- (void)setSoftLogoutCredentials:(MXCredentials *)credentials
{
softLogoutCredentials = credentials;
self.userLoginTextField.text = softLogoutCredentials.userId;
self.userLoginContainer.hidden = YES;
self.phoneContainer.hidden = YES;
[self displaySoftLogoutMessage];
}
- (void)displaySoftLogoutMessage
{
// Take some shortcuts and make some assumptions (Riot uses MXFileStore and MXRealmCryptoStore) to
// retrieve data to display as quick as possible
MXRealmCryptoStore *cryptoStore = [[MXRealmCryptoStore alloc] initWithCredentials:self.softLogoutCredentials];
BOOL keyBackupNeeded = [cryptoStore inboundGroupSessionsToBackup:1].count > 0;
MXFileStore *fileStore = [[MXFileStore alloc] initWithCredentials:softLogoutCredentials];
[fileStore asyncUsersWithUserIds:@[softLogoutCredentials.userId] success:^(NSArray<MXUser *> * _Nonnull users) {
MXUser *myUser = users.firstObject;
[fileStore close];
[self displaySoftLogoutMessageWithUserDisplayname:myUser.displayname andKeyBackupNeeded:keyBackupNeeded];
} failure:^(NSError * _Nonnull error) {
NSLog(@"[AuthInputsView] displaySoftLogoutMessage: Cannot load displayname. Error: %@", error);
[self displaySoftLogoutMessageWithUserDisplayname:nil andKeyBackupNeeded:keyBackupNeeded];
}];
}
- (void)displaySoftLogoutMessageWithUserDisplayname:(NSString*)userDisplayname andKeyBackupNeeded:(BOOL)keyBackupNeeded
{
// Use messageLabel for this message
self.messageLabelTopConstraint.constant = 8;
self.messageLabel.textColor = ThemeService.shared.theme.textPrimaryColor;
self.messageLabel.hidden = NO;
NSMutableAttributedString *message = [[NSMutableAttributedString alloc] initWithString:NSLocalizedStringFromTable(@"auth_softlogout_sign_in", @"Vector", nil)
attributes:@{
NSFontAttributeName: [UIFont boldSystemFontOfSize:14]
}];
[message appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\n"]];
NSString *string = [NSString stringWithFormat:NSLocalizedStringFromTable(@"auth_softlogout_reason", @"Vector", nil),
softLogoutCredentials.homeServerName, userDisplayname, softLogoutCredentials.userId];
[message appendAttributedString:[[NSAttributedString alloc] initWithString:string
attributes:@{
NSFontAttributeName: [UIFont systemFontOfSize:14]
}]];
if (keyBackupNeeded)
{
[message appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\n"]];
string = NSLocalizedStringFromTable(@"auth_softlogout_recover_encryption_keys", @"Vector", nil);
[message appendAttributedString:[[NSAttributedString alloc] initWithString:string
attributes:@{
NSFontAttributeName: [UIFont systemFontOfSize:14]
}]];
}
self.messageLabel.attributedText = message;
}
- (BOOL)areAllRequiredFieldsSet
{
// Keep enable the submit button.

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -15,7 +15,7 @@
<rect key="frame" x="0.0" y="0.0" width="600" height="200"/>
<subviews>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ckH-mV-Yds">
<rect key="frame" x="0.0" y="8.5" width="600" height="183"/>
<rect key="frame" x="0.0" y="37.5" width="600" height="183"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fSJ-23-bFN">
<rect key="frame" x="180" y="75.5" width="240" height="32"/>
@ -284,6 +284,7 @@
<constraint firstItem="rb1-L5-udI" firstAttribute="top" secondItem="x74-04-ezp" secondAttribute="top" constant="100" id="75U-tx-PsQ"/>
<constraint firstItem="UfH-jv-6w4" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" id="7Bk-GF-MZ0"/>
<constraint firstItem="IB7-1E-eeL" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" id="7Lr-fy-W8L"/>
<constraint firstItem="ckH-mV-Yds" firstAttribute="top" relation="greaterThanOrEqual" secondItem="68j-f9-JG4" secondAttribute="bottom" constant="10" id="7aQ-ei-TrU"/>
<constraint firstAttribute="trailing" secondItem="68j-f9-JG4" secondAttribute="trailing" constant="10" id="8Aa-YT-MP5"/>
<constraint firstAttribute="trailing" secondItem="UfH-jv-6w4" secondAttribute="trailing" id="8dz-wY-Kxx"/>
<constraint firstItem="whs-Ob-uzD" firstAttribute="centerX" secondItem="x74-04-ezp" secondAttribute="centerX" id="8lX-k1-85c"/>
@ -293,7 +294,7 @@
<constraint firstItem="bXz-VI-5FS" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" id="Frq-sH-HZT"/>
<constraint firstItem="xOW-lo-QGC" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" id="NOu-LR-RvE"/>
<constraint firstAttribute="trailing" secondItem="bXz-VI-5FS" secondAttribute="trailing" id="NiV-pJ-PfV"/>
<constraint firstItem="ckH-mV-Yds" firstAttribute="centerY" secondItem="x74-04-ezp" secondAttribute="centerY" id="PNx-SK-FZY"/>
<constraint firstItem="ckH-mV-Yds" firstAttribute="centerY" secondItem="x74-04-ezp" secondAttribute="centerY" priority="100" id="PNx-SK-FZY"/>
<constraint firstAttribute="trailing" secondItem="xOW-lo-QGC" secondAttribute="trailing" id="SNm-WQ-Piu"/>
<constraint firstItem="xOW-lo-QGC" firstAttribute="top" secondItem="x74-04-ezp" secondAttribute="top" id="WmX-gO-hPJ"/>
<constraint firstItem="rb1-L5-udI" firstAttribute="leading" secondItem="x74-04-ezp" secondAttribute="leading" id="XAJ-ST-sWV"/>

View file

@ -0,0 +1,187 @@
/*
Copyright 2019 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 AVFoundation
@objc protocol CameraPresenterDelegate: class {
func cameraPresenter(_ presenter: CameraPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?)
func cameraPresenter(_ presenter: CameraPresenter, didSelectVideoAt url: URL)
func cameraPresenterDidCancel(_ cameraPresenter: CameraPresenter)
}
/// CameraPresenter enables to present native camera
@objc final class CameraPresenter: NSObject {
// MARK: - Constants
private enum Constants {
static let jpegCompressionQuality: CGFloat = 1.0
}
// MARK: - Properties
// MARK: - Private
private weak var presentingViewController: UIViewController?
private weak var cameraViewController: UIViewController?
private var mediaUTIs: [MXKUTI] = []
// MARK: - Public
@objc weak var delegate: CameraPresenterDelegate?
// MARK: - Public
@objc func presentCamera(from presentingViewController: UIViewController, with mediaUTIs: [MXKUTI], animated: Bool) {
self.presentingViewController = presentingViewController
self.mediaUTIs = mediaUTIs
self.checkCameraPermissionAndPresentCamera(animated: animated)
}
@objc func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let cameraViewController = self.cameraViewController else {
return
}
cameraViewController.dismiss(animated: animated, completion: completion)
}
// MARK: - Private
private func checkCameraPermissionAndPresentCamera(animated: Bool) {
let authorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch authorizationStatus {
case .authorized:
self.presentCameraController(animated: animated)
case .notDetermined:
self.requestCameraAccess(completion: { (granted) in
if granted {
self.presentCameraController(animated: animated)
} else {
self.presentPermissionDeniedAlert()
}
})
case .denied, .restricted:
self.presentPermissionDeniedAlert()
@unknown default:
break
}
}
private func presentCameraController(animated: Bool) {
guard let presentingViewController = self.presentingViewController else {
return
}
guard let cameraViewController = self.buildCameraViewController() else {
return
}
presentingViewController.present(cameraViewController, animated: true, completion: nil)
self.cameraViewController = cameraViewController
}
private func buildCameraViewController() -> UIViewController? {
guard UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) else {
return nil
}
let mediaTypes = self.mediaUTIs.map { (uti) -> String in
return uti.rawValue
}
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = self
imagePickerController.sourceType = UIImagePickerController.SourceType.camera
imagePickerController.mediaTypes = mediaTypes
imagePickerController.allowsEditing = false
return imagePickerController
}
private func requestCameraAccess(completion: @escaping (_ granted: Bool) -> Void) {
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
completion(granted)
}
}
}
private func presentPermissionDeniedAlert() {
guard let presentingViewController = self.presentingViewController, let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
return
}
let appDisplayName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? ""
let alert = UIAlertController(title: VectorL10n.camera, message: VectorL10n.cameraAccessNotGranted(appDisplayName), preferredStyle: .alert)
let cancelActionTitle = Bundle.mxk_localizedString(forKey: "ok")
let cancelAction = UIAlertAction(title: cancelActionTitle, style: .cancel, handler: { _ in
})
let settingsActionTitle = Bundle.mxk_localizedString(forKey: "settings")
let settingsAction = UIAlertAction(title: settingsActionTitle, style: .default, handler: { _ in
UIApplication.shared.open(settingsURL, options: [:], completionHandler: { (succeed) in
if !succeed {
print("[CameraPresenter] Fails to open settings")
}
})
})
alert.addAction(cancelAction)
alert.addAction(settingsAction)
presentingViewController.present(alert, animated: true, completion: nil)
}
private func presentCameraUnavailableAlert() {
guard let presentingViewController = self.presentingViewController else {
return
}
let alert = UIAlertController(title: VectorL10n.camera, message: VectorL10n.cameraUnavailable, preferredStyle: .alert)
let okAction = UIAlertAction(title: VectorL10n.accept, style: .default, handler: nil)
alert.addAction(okAction)
presentingViewController.present(alert, animated: true, completion: nil)
}
}
// MARK: - UIImagePickerControllerDelegate
extension CameraPresenter: UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let videoURL = info[.mediaURL] as? URL {
self.delegate?.cameraPresenter(self, didSelectVideoAt: videoURL)
} else if let image = (info[.editedImage] ?? info[.originalImage]) as? UIImage, let imageData = image.jpegData(compressionQuality: Constants.jpegCompressionQuality) {
self.delegate?.cameraPresenter(self, didSelectImageData: imageData, withUTI: MXKUTI.jpeg)
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.delegate?.cameraPresenterDidCancel(self)
}
}
// MARK: - UINavigationControllerDelegate
extension CameraPresenter: UINavigationControllerDelegate {
}

View file

@ -1205,7 +1205,7 @@
// FIXME: this all seems incredibly fragile and tied to gutwrenching the current UISearchBar internals.
// text color
UITextField *searchBarTextField = [searchBar valueForKey:@"_searchField"];
UITextField *searchBarTextField = searchBar.vc_searchTextField;
searchBarTextField.textColor = ThemeService.shared.theme.textSecondaryColor;
// Magnifying glass icon.

View file

@ -606,7 +606,7 @@
// FIXME: this all seems incredibly fragile and tied to gutwrenching the current UISearchBar internals.
// text color
UITextField *searchBarTextField = [searchBar valueForKey:@"_searchField"];
UITextField *searchBarTextField = searchBar.vc_searchTextField;
searchBarTextField.textColor = ThemeService.shared.theme.textSecondaryColor;
// Magnifying glass icon.

View file

@ -69,7 +69,7 @@
/**
The delegate for the view controller.
*/
@property (nonatomic) id<MediaAlbumContentViewControllerDelegate> delegate;
@property (nonatomic, weak) id<MediaAlbumContentViewControllerDelegate> delegate;
/**
The array of the media types listed by the view controller (default value is an array containing kUTTypeImage).

View file

@ -89,11 +89,15 @@
self.mediaTypes = @[(NSString *)kUTTypeImage];
}
MXWeakify(self);
// Observe UIApplicationWillEnterForegroundNotification to refresh captures collection when app leaves the background state.
UIApplicationWillEnterForegroundNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXStrongifyAndReturnIfNil(self);
// Force a full refresh of the displayed collection
self.assetsCollection = _assetsCollection;
self.assetsCollection = self->_assetsCollection;
}];
@ -105,6 +109,8 @@
// Observe user interface theme change.
kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXStrongifyAndReturnIfNil(self);
[self userInterfaceThemeDidChange];
}];

View file

@ -0,0 +1,97 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh Test MediaPicker
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
final class MediaPickerCoordinator: NSObject, MediaPickerCoordinatorType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let mediaUTIs: [MXKUTI]
private let allowsMultipleSelection: Bool
private let navigationRouter: NavigationRouterType
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: MediaPickerCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, mediaUTIs: [MXKUTI], allowsMultipleSelection: Bool) {
self.session = session
self.mediaUTIs = mediaUTIs
self.allowsMultipleSelection = allowsMultipleSelection
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
super.init()
}
// MARK: - Public methods
func start() {
let mediaTypes = self.mediaUTIs.map { (uti) -> String in
return uti.rawValue
}
let mediaPickerViewController: MediaPickerViewController = MediaPickerViewController.instantiate()
mediaPickerViewController.mediaTypes = mediaTypes
mediaPickerViewController.allowsMultipleSelection = self.allowsMultipleSelection
self.navigationRouter.setRootModule(mediaPickerViewController)
mediaPickerViewController.delegate = self
}
func toPresentable() -> UIViewController {
return self.navigationRouter.toPresentable()
}
}
// MARK: - MediaPickerViewControllerDelegate
extension MediaPickerCoordinator: MediaPickerViewControllerDelegate {
func mediaPickerController(_ mediaPickerController: MediaPickerViewController!, didSelectImage imageData: Data!, withMimeType mimetype: String!, isPhotoLibraryAsset: Bool) {
let uti: MXKUTI?
if let mimetype = mimetype {
uti = MXKUTI(mimeType: mimetype)
} else {
uti = nil
}
self.delegate?.mediaPickerCoordinator(self, didSelectImageData: imageData, withUTI: uti)
}
func mediaPickerController(_ mediaPickerController: MediaPickerViewController!, didSelectVideo videoURL: URL!) {
self.delegate?.mediaPickerCoordinator(self, didSelectVideoAt: videoURL)
}
func mediaPickerController(_ mediaPickerController: MediaPickerViewController!, didSelect assets: [PHAsset]!) {
self.delegate?.mediaPickerCoordinator(self, didSelectAssets: assets)
}
func mediaPickerControllerDidCancel(_ mediaPickerController: MediaPickerViewController!) {
self.delegate?.mediaPickerCoordinatorDidCancel(self)
}
}

View file

@ -0,0 +1,124 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh Test MediaPicker
/*
Copyright 2019 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
@objc protocol MediaPickerCoordinatorBridgePresenterDelegate {
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?)
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectVideoAt url: URL)
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectAssets assets: [PHAsset])
func mediaPickerCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter)
}
/// MediaPickerCoordinatorBridgePresenter enables to start MediaPickerCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
@objcMembers
final class MediaPickerCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let mediaUTIs: [MXKUTI]
private let allowsMultipleSelection: Bool
private var coordinator: MediaPickerCoordinator?
// MARK: Public
weak var delegate: MediaPickerCoordinatorBridgePresenterDelegate?
// MARK: - Setup
init(session: MXSession, mediaUTIs: [MXKUTI], allowsMultipleSelection: Bool) {
self.session = session
self.mediaUTIs = mediaUTIs
self.allowsMultipleSelection = allowsMultipleSelection
super.init()
}
// MARK: - Public
func present(from viewController: UIViewController,
sourceView: UIView?,
sourceRect: CGRect,
animated: Bool) {
let mediaPickerCoordinator = MediaPickerCoordinator(session: self.session, mediaUTIs: mediaUTIs, allowsMultipleSelection: self.allowsMultipleSelection)
mediaPickerCoordinator.delegate = self
let mediaPickerPresentable = mediaPickerCoordinator.toPresentable()
if let sourceView = sourceView {
mediaPickerPresentable.modalPresentationStyle = .popover
if let popoverPresentationController = mediaPickerPresentable.popoverPresentationController {
popoverPresentationController.sourceView = sourceView
let finalSourceRect: CGRect
if sourceRect != CGRect.null {
finalSourceRect = sourceRect
} else {
finalSourceRect = sourceView.bounds
}
popoverPresentationController.sourceRect = finalSourceRect
}
}
viewController.present(mediaPickerPresentable, animated: animated, completion: nil)
mediaPickerCoordinator.start()
self.coordinator = mediaPickerCoordinator
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
if let completion = completion {
completion()
}
}
}
}
// MARK: - MediaPickerCoordinatorDelegate
extension MediaPickerCoordinatorBridgePresenter: MediaPickerCoordinatorDelegate {
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) {
self.delegate?.mediaPickerCoordinatorBridgePresenter(self, didSelectImageData: imageData, withUTI: uti)
}
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectVideoAt url: URL) {
self.delegate?.mediaPickerCoordinatorBridgePresenter(self, didSelectVideoAt: url)
}
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectAssets assets: [PHAsset]) {
self.delegate?.mediaPickerCoordinatorBridgePresenter(self, didSelectAssets: assets)
}
func mediaPickerCoordinatorDidCancel(_ coordinator: MediaPickerCoordinatorType) {
self.delegate?.mediaPickerCoordinatorBridgePresenterDidCancel(self)
}
}

View file

@ -0,0 +1,31 @@
// File created from FlowTemplate
// $ createRootCoordinator.sh Test MediaPicker
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol MediaPickerCoordinatorDelegate: class {
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectImageData imageData: Data, withUTI uti: MXKUTI?)
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectVideoAt url: URL)
func mediaPickerCoordinator(_ coordinator: MediaPickerCoordinatorType, didSelectAssets assets: [PHAsset])
func mediaPickerCoordinatorDidCancel(_ coordinator: MediaPickerCoordinatorType)
}
/// `MediaPickerCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow.
protocol MediaPickerCoordinatorType: Coordinator, Presentable {
var delegate: MediaPickerCoordinatorDelegate? { get }
}

View file

@ -43,6 +43,13 @@
*/
- (void)mediaPickerController:(MediaPickerViewController *)mediaPickerController didSelectVideo:(NSURL*)videoURL;
/**
Tells the delegate that the user wants to cancel media picking.
@param mediaPickerController the `MediaPickerViewController` instance.
*/
- (void)mediaPickerControllerDidCancel:(MediaPickerViewController *)mediaPickerController;
@optional
/**
Tells the delegate that the user select multiple media.
@ -55,16 +62,9 @@
@end
/**
* MediaPickerViewController displays recent camera captures and photo/video albums from user library.
*/
@interface MediaPickerViewController : MXKViewController <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, AVCaptureFileOutputRecordingDelegate, MediaAlbumContentViewControllerDelegate>
/**
* Returns the `UINib` object initialized for a `MediaPickerViewController`.
*
* @return The initialized `UINib` object or `nil` if there were errors during initialization
* or the nib file could not be located.
*/
+ (UINib *)nib;
@interface MediaPickerViewController : MXKViewController
/**
* Creates and returns a new `MediaPickerViewController` object.
@ -73,7 +73,7 @@
*
* @return An initialized `MediaPickerViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)mediaPickerViewController;
+ (instancetype)instantiate;
/**
The delegate for the view controller.
@ -85,5 +85,11 @@
*/
@property (nonatomic) NSArray *mediaTypes;
/**
A Boolean value that determines whether users can select more than one item.
Default is NO.
*/
@property (nonatomic) BOOL allowsMultipleSelection;
@end

File diff suppressed because it is too large Load diff

View file

@ -1,26 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14113" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina5_5" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MediaPickerViewController">
<connections>
<outlet property="cameraActivityIndicator" destination="dxP-iB-dWk" id="HBu-hw-Cx9"/>
<outlet property="cameraCaptureButton" destination="uRG-b0-CjY" id="3Qc-PU-LUY"/>
<outlet property="cameraCaptureButtonWidthConstraint" destination="gmN-48-HHd" id="NQ1-3b-Oz7"/>
<outlet property="cameraPreviewContainerAspectRatio" destination="QDd-Tq-4zL" id="qNb-5w-GnM"/>
<outlet property="cameraPreviewContainerView" destination="w7z-f3-kdT" id="iqZ-I1-ZcW"/>
<outlet property="cameraSwitchButton" destination="PXk-ZD-TYS" id="i6p-yB-8HG"/>
<outlet property="cameraVideoCaptureProgressView" destination="aXP-Vh-zUp" id="yvj-G0-8oa"/>
<outlet property="captureViewContainer" destination="cE0-0g-Su5" id="cr5-6S-9rj"/>
<outlet property="closeButton" destination="sO8-Ds-mXZ" id="lkx-5W-NN9"/>
<outlet property="libraryViewContainer" destination="QeN-lu-6hK" id="hHu-hn-fXS"/>
<outlet property="libraryViewContainerViewHeightConstraint" destination="aRE-Ox-5IJ" id="6mr-qM-Oge"/>
<outlet property="mainScrollView" destination="ZfS-wG-RjJ" id="Jjn-q7-ggw"/>
@ -37,7 +28,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ZfS-wG-RjJ">
<rect key="frame" x="0.0" y="0.0" width="414" height="736"/>
<rect key="frame" x="0.0" y="20" width="414" height="716"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cE0-0g-Su5" userLabel="Capture View">
<rect key="frame" x="0.0" y="0.0" width="414" height="607.33333333333337"/>
@ -58,9 +49,6 @@
<state key="normal">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="xHg-kl-L3w"/>
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="camera_switch" translatesAutoresizingMaskIntoConstraints="NO" id="etc-h5-0u2">
<rect key="frame" x="363" y="35" width="32" height="24"/>
@ -80,9 +68,6 @@
<state key="normal">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="nvH-aw-ZJK"/>
</connections>
</button>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="remove_icon" translatesAutoresizingMaskIntoConstraints="NO" id="VA2-Cu-dX6">
<rect key="frame" x="24" y="36" width="22" height="22"/>
@ -92,10 +77,10 @@
</constraints>
</imageView>
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="dxP-iB-dWk" userLabel="Camera Activity Indicator">
<rect key="frame" x="197" y="294" width="20" height="20"/>
<rect key="frame" x="197" y="293.66666666666669" width="20" height="20"/>
</activityIndicatorView>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aXP-Vh-zUp" customClass="MXKPieChartView">
<rect key="frame" x="161" y="490" width="92" height="92"/>
<rect key="frame" x="161" y="489.66666666666674" width="92" height="92"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="MediaPickerVCCameraVideoCaptureProgressView"/>
<constraints>
@ -114,9 +99,6 @@
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted" image="camera_capture"/>
<connections>
<action selector="onButtonPressed:" destination="-1" eventType="touchUpInside" id="R4P-UW-q4j"/>
</connections>
</button>
</subviews>
<accessibility key="accessibilityConfiguration" identifier="accessibilityIdentifierCaptureView"/>
@ -143,7 +125,7 @@
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jzs-FL-Rqd" userLabel="Collection Container View">
<rect key="frame" x="0.0" y="607.33333333333337" width="414" height="450.00000000000011"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="450"/>
<subviews>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="none" translatesAutoresizingMaskIntoConstraints="NO" id="Cnz-mP-gE5">
<rect key="frame" x="0.0" y="2" width="414" height="448"/>
@ -173,7 +155,7 @@
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QeN-lu-6hK" userLabel="Library View">
<rect key="frame" x="0.0" y="1057.3333333333333" width="414" height="74"/>
<rect key="frame" x="0.0" y="450" width="414" height="74"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="74" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="mCG-GH-ADc" userLabel="Albums Table View">
<rect key="frame" x="0.0" y="0.0" width="414" height="74"/>
@ -199,6 +181,7 @@
</subviews>
<constraints>
<constraint firstItem="cE0-0g-Su5" firstAttribute="top" secondItem="ZfS-wG-RjJ" secondAttribute="top" id="1Kz-qe-p3Y"/>
<constraint firstItem="jzs-FL-Rqd" firstAttribute="width" secondItem="ZfS-wG-RjJ" secondAttribute="width" id="1fS-n9-T98"/>
<constraint firstItem="QeN-lu-6hK" firstAttribute="top" secondItem="jzs-FL-Rqd" secondAttribute="bottom" id="3az-9S-rxJ"/>
<constraint firstItem="cE0-0g-Su5" firstAttribute="centerX" secondItem="ZfS-wG-RjJ" secondAttribute="centerX" id="Fqd-7G-H68"/>
<constraint firstItem="cE0-0g-Su5" firstAttribute="leading" secondItem="ZfS-wG-RjJ" secondAttribute="leading" id="IZE-p0-8AN"/>
@ -208,12 +191,18 @@
<constraint firstAttribute="trailing" secondItem="jzs-FL-Rqd" secondAttribute="trailing" id="g7G-DC-uSR"/>
<constraint firstAttribute="trailing" secondItem="QeN-lu-6hK" secondAttribute="trailing" id="kbe-Uh-tug"/>
<constraint firstItem="jzs-FL-Rqd" firstAttribute="top" secondItem="cE0-0g-Su5" secondAttribute="bottom" id="q35-tT-GBd"/>
<constraint firstItem="jzs-FL-Rqd" firstAttribute="top" secondItem="ZfS-wG-RjJ" secondAttribute="top" id="qwZ-4d-IVv"/>
<constraint firstItem="cE0-0g-Su5" firstAttribute="height" relation="lessThanOrEqual" secondItem="ZfS-wG-RjJ" secondAttribute="height" id="w7s-40-3O5"/>
<constraint firstItem="jzs-FL-Rqd" firstAttribute="leading" secondItem="ZfS-wG-RjJ" secondAttribute="leading" id="wZf-nP-JTD"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="accessibilityIdentifier" value="MediaPickerVCMainScrollView"/>
</userDefinedRuntimeAttributes>
<variation key="default">
<mask key="subviews">
<exclude reference="cE0-0g-Su5"/>
</mask>
</variation>
<connections>
<outlet property="delegate" destination="-1" id="Wso-Tg-NuE"/>
</connections>
@ -222,16 +211,18 @@
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="MediaPickerVCView"/>
<constraints>
<constraint firstItem="ZfS-wG-RjJ" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="BJx-Hb-D6M"/>
<constraint firstItem="a75-G6-NXm" firstAttribute="top" secondItem="ZfS-wG-RjJ" secondAttribute="top" id="K0g-Ml-d3i"/>
<constraint firstAttribute="bottom" secondItem="ZfS-wG-RjJ" secondAttribute="bottom" id="Sre-5e-ocp"/>
<constraint firstItem="ZfS-wG-RjJ" firstAttribute="width" secondItem="iN0-l3-epB" secondAttribute="width" id="TFD-H5-ucr"/>
<constraint firstAttribute="trailing" secondItem="ZfS-wG-RjJ" secondAttribute="trailing" id="UrC-gh-xoR"/>
<constraint firstItem="ZfS-wG-RjJ" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="bUn-mA-Mv2"/>
</constraints>
<viewLayoutGuide key="safeArea" id="a75-G6-NXm"/>
</view>
</objects>
<resources>
<image name="camera_capture" width="83" height="83"/>
<image name="camera_switch" width="32" height="24"/>
<image name="camera_switch" width="31.333333969116211" height="23.666666030883789"/>
<image name="remove_icon" width="24" height="24"/>
</resources>
</document>

View file

@ -0,0 +1,150 @@
/*
Copyright 2019 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 AVFoundation
@objc protocol SingleImagePickerPresenterDelegate: class {
func singleImagePickerPresenter(_ presenter: SingleImagePickerPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?)
func singleImagePickerPresenterDidCancel(_ presenter: SingleImagePickerPresenter)
}
/// SingleImagePickerPresenter enables to present an image picker with single selection
@objcMembers
final class SingleImagePickerPresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private weak var presentingViewController: UIViewController?
private var cameraPresenter: CameraPresenter?
private var mediaPickerPresenter: MediaPickerCoordinatorBridgePresenter?
// MARK: Public
weak var delegate: SingleImagePickerPresenterDelegate?
// MARK: - Setup
init(session: MXSession) {
self.session = session
}
// MARK: - Public
func present(from presentingViewController: UIViewController,
sourceView: UIView?,
sourceRect: CGRect,
animated: Bool) {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let cameraAction = UIAlertAction(title: VectorL10n.imagePickerActionCamera, style: .default, handler: { _ in
self.presentCamera(animated: animated)
})
let photoLibraryAction = UIAlertAction(title: VectorL10n.imagePickerActionLibrary, style: .default, handler: { _ in
self.presentPhotoLibray(sourceView: sourceView, sourceRect: sourceRect, animated: animated)
})
let cancelAction = UIAlertAction(title: VectorL10n.cancel, style: .cancel, handler: { _ in
})
alert.addAction(cameraAction)
alert.addAction(photoLibraryAction)
alert.addAction(cancelAction)
if let popoverPresentationController = alert.popoverPresentationController {
popoverPresentationController.sourceView = sourceView
popoverPresentationController.sourceRect = sourceRect
}
presentingViewController.present(alert, animated: animated, completion: nil)
self.presentingViewController = presentingViewController
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
if let cameraPresenter = self.cameraPresenter {
cameraPresenter.dismiss(animated: animated, completion: completion)
} else if let mediaPickerPresenter = self.mediaPickerPresenter {
mediaPickerPresenter.dismiss(animated: animated, completion: completion)
}
}
// MARK: - Private
private func presentCamera(animated: Bool) {
guard let presentingViewController = self.presentingViewController else {
return
}
let cameraPresenter = CameraPresenter()
cameraPresenter.delegate = self
cameraPresenter.presentCamera(from: presentingViewController, with: [.image], animated: animated)
self.cameraPresenter = cameraPresenter
}
private func presentPhotoLibray(sourceView: UIView?, sourceRect: CGRect, animated: Bool) {
guard let presentingViewController = self.presentingViewController else {
return
}
let mediaPickerPresenter = MediaPickerCoordinatorBridgePresenter(session: self.session, mediaUTIs: [.image], allowsMultipleSelection: false)
mediaPickerPresenter.delegate = self
mediaPickerPresenter.present(from: presentingViewController, sourceView: sourceView, sourceRect: sourceRect, animated: animated)
self.mediaPickerPresenter = mediaPickerPresenter
}
}
// MARK: - CameraPresenterDelegate
extension SingleImagePickerPresenter: CameraPresenterDelegate {
func cameraPresenter(_ cameraPresenter: CameraPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) {
self.delegate?.singleImagePickerPresenter(self, didSelectImageData: imageData, withUTI: uti)
}
func cameraPresenterDidCancel(_ cameraPresenter: CameraPresenter) {
self.delegate?.singleImagePickerPresenterDidCancel(self)
}
func cameraPresenter(_ cameraPresenter: CameraPresenter, didSelectVideoAt url: URL) {
self.delegate?.singleImagePickerPresenterDidCancel(self)
}
}
// MARK: - MediaPickerCoordinatorBridgePresenterDelegate
extension SingleImagePickerPresenter: MediaPickerCoordinatorBridgePresenterDelegate {
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) {
self.delegate?.singleImagePickerPresenter(self, didSelectImageData: imageData, withUTI: uti)
}
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectVideoAt url: URL) {
self.delegate?.singleImagePickerPresenterDidCancel(self)
}
func mediaPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter, didSelectAssets assets: [PHAsset]) {
self.delegate?.singleImagePickerPresenterDidCancel(self)
}
func mediaPickerCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: MediaPickerCoordinatorBridgePresenter) {
self.delegate?.singleImagePickerPresenterDidCancel(self)
}
}

View file

@ -54,20 +54,8 @@ final class BubbleReactionsView: UIView, NibOwnerLoadable {
// MARK: - Setup
private func commonInit() {
self.collectionView.isScrollEnabled = false
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.collectionViewLayout = DGCollectionViewLeftAlignFlowLayout()
if let collectionViewFlowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
collectionViewFlowLayout.minimumInteritemSpacing = Constants.minimumInteritemSpacing
collectionViewFlowLayout.minimumLineSpacing = Constants.minimumLineSpacing
}
self.collectionView.register(cellType: BubbleReactionViewCell.self)
self.collectionView.register(cellType: BubbleReactionActionViewCell.self)
self.collectionView.reloadData()
self.setupCollectionView()
self.setupLongPressGestureRecognizer()
}
convenience init() {
@ -95,6 +83,36 @@ final class BubbleReactionsView: UIView, NibOwnerLoadable {
// MARK: - Private
private func setupCollectionView() {
self.collectionView.isScrollEnabled = false
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.collectionViewLayout = DGCollectionViewLeftAlignFlowLayout()
if let collectionViewFlowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
collectionViewFlowLayout.minimumInteritemSpacing = Constants.minimumInteritemSpacing
collectionViewFlowLayout.minimumLineSpacing = Constants.minimumLineSpacing
}
self.collectionView.register(cellType: BubbleReactionViewCell.self)
self.collectionView.register(cellType: BubbleReactionActionViewCell.self)
self.collectionView.reloadData()
}
private func setupLongPressGestureRecognizer() {
let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
gestureRecognizer.delaysTouchesBegan = true
self.collectionView.addGestureRecognizer(gestureRecognizer)
}
@objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
guard gestureRecognizer.state == .began else {
return
}
self.viewModel?.process(viewAction: .longPress)
}
private func fill(reactionsViewData: [BubbleReactionViewData], showAllButtonState: BubbleReactionsViewState.ShowAllButtonState) {
self.reactionsViewData = reactionsViewData
self.showAllButtonState = showAllButtonState

View file

@ -69,6 +69,8 @@ import Foundation
self.viewModelDelegate?.bubbleReactionsViewModel(self, didShowAllTappedForEventId: self.eventId)
case .tapShowAction(.showLess):
self.viewModelDelegate?.bubbleReactionsViewModel(self, didShowLessTappedForEventId: self.eventId)
case .longPress:
self.viewModelDelegate?.bubbleReactionsViewModel(self, didLongPressForEventId: self.eventId)
}
}

View file

@ -21,6 +21,7 @@ enum BubbleReactionsViewAction {
case tapReaction(index: Int)
case addNewReaction
case tapShowAction(action: ShowAction)
case longPress
enum ShowAction {
case showAll
@ -43,6 +44,7 @@ enum BubbleReactionsViewState {
func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didRemoveReaction reactionCount: MXReactionCount, forEventId eventId: String)
func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didShowAllTappedForEventId eventId: String)
func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didShowLessTappedForEventId eventId: String)
func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didLongPressForEventId eventId: String)
}
protocol BubbleReactionsViewModelViewDelegate: class {

View file

@ -30,7 +30,9 @@ final class ReactionsMenuView: UIView, Themable, NibLoadable {
// MARK: Outlets
@IBOutlet private weak var reactionsBackgroundView: UIView!
@IBOutlet private weak var reactionsStackView: UIStackView!
@IBOutlet private weak var reactionsStackView: UIStackView!
@IBOutlet private weak var moreReactionsBackgroundView: UIView!
@IBOutlet private weak var moreReactionsButton: UIButton!
// MARK: Private
@ -57,6 +59,10 @@ final class ReactionsMenuView: UIView, Themable, NibLoadable {
super.awakeFromNib()
self.reactionsBackgroundView.layer.masksToBounds = true
let moreReactionsImage = Asset.Images.moreReactions.image.withRenderingMode(.alwaysTemplate)
self.moreReactionsButton.setImage(moreReactionsImage, for: .normal)
self.update(theme: ThemeService.shared().theme)
}
@ -64,12 +70,15 @@ final class ReactionsMenuView: UIView, Themable, NibLoadable {
super.layoutSubviews()
self.reactionsBackgroundView.layer.cornerRadius = self.reactionsBackgroundView.frame.size.height/2
self.moreReactionsBackgroundView.layer.cornerRadius = self.moreReactionsBackgroundView.frame.size.height/2
}
// MARK: - Public
func update(theme: Theme) {
self.reactionsBackgroundView.backgroundColor = theme.headerBackgroundColor
self.moreReactionsBackgroundView.backgroundColor = theme.headerBackgroundColor
self.moreReactionsButton.tintColor = theme.textPrimaryColor
}
func selectionAnimationInstructionPart1() {
@ -132,6 +141,10 @@ final class ReactionsMenuView: UIView, Themable, NibLoadable {
self.tappedReactionButton = sender
self.viewModel?.process(viewAction: .tap(reaction: tappedReaction))
}
@IBAction private func moreReactionsAction(_ sender: Any) {
self.viewModel?.process(viewAction: .moreReactions)
}
}
// MARK: - ReactionsMenuViewModelViewDelegate

View file

@ -16,36 +16,66 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eXy-Kc-Ck7">
<rect key="frame" x="0.0" y="0.0" width="459" height="59"/>
<rect key="frame" x="0.0" y="0.0" width="395" height="59"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="uzd-VB-mKT">
<rect key="frame" x="5" y="5" width="449" height="49"/>
<rect key="frame" x="5" y="5" width="385" height="49"/>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="uzd-VB-mKT" secondAttribute="trailing" constant="5" id="fDZ-NX-EyG"/>
<constraint firstItem="uzd-VB-mKT" firstAttribute="leading" secondItem="eXy-Kc-Ck7" secondAttribute="leading" constant="5" id="iCa-Ob-e7J"/>
<constraint firstAttribute="trailing" secondItem="uzd-VB-mKT" secondAttribute="trailing" constant="5" id="lG6-Qg-UcU"/>
<constraint firstItem="uzd-VB-mKT" firstAttribute="top" secondItem="eXy-Kc-Ck7" secondAttribute="top" constant="5" id="msb-Ay-Bp6"/>
<constraint firstAttribute="bottom" secondItem="uzd-VB-mKT" secondAttribute="bottom" constant="5" id="nJE-Gm-Rf1"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="44R-pM-RoU">
<rect key="frame" x="400" y="0.0" width="59" height="59"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="VQg-zY-j1F">
<rect key="frame" x="0.0" y="0.0" width="59" height="59"/>
<state key="normal" image="more_reactions">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="moreReactionsAction:" destination="7Xq-Wy-z0M" eventType="touchUpInside" id="09O-bn-ezs"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="VQg-zY-j1F" firstAttribute="top" secondItem="44R-pM-RoU" secondAttribute="top" id="Ku5-Ra-6tc"/>
<constraint firstItem="VQg-zY-j1F" firstAttribute="leading" secondItem="44R-pM-RoU" secondAttribute="leading" id="YpY-1A-KhO"/>
<constraint firstAttribute="trailing" secondItem="VQg-zY-j1F" secondAttribute="trailing" id="aYC-Po-gf0"/>
<constraint firstAttribute="bottom" secondItem="VQg-zY-j1F" secondAttribute="bottom" id="fkS-hS-S5L"/>
<constraint firstAttribute="width" secondItem="44R-pM-RoU" secondAttribute="height" multiplier="1:1" id="vLH-qD-4s0"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="eXy-Kc-Ck7" firstAttribute="top" secondItem="7Xq-Wy-z0M" secondAttribute="top" id="43J-YM-JHg"/>
<constraint firstItem="eXy-Kc-Ck7" firstAttribute="leading" secondItem="7Xq-Wy-z0M" secondAttribute="leading" id="9dy-sK-ygm"/>
<constraint firstAttribute="trailing" secondItem="eXy-Kc-Ck7" secondAttribute="trailing" id="E4Z-bt-BRw"/>
<constraint firstAttribute="bottom" secondItem="eXy-Kc-Ck7" secondAttribute="bottom" id="Nb2-0z-IiS"/>
<constraint firstItem="44R-pM-RoU" firstAttribute="top" secondItem="7Xq-Wy-z0M" secondAttribute="top" id="aLI-UY-XKE"/>
<constraint firstAttribute="trailing" secondItem="44R-pM-RoU" secondAttribute="trailing" id="fKs-oS-uui"/>
<constraint firstAttribute="bottom" secondItem="44R-pM-RoU" secondAttribute="bottom" id="lE1-4e-whG"/>
<constraint firstItem="44R-pM-RoU" firstAttribute="leading" secondItem="eXy-Kc-Ck7" secondAttribute="trailing" constant="5" id="zFB-XG-OKN"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="moreReactionsBackgroundView" destination="44R-pM-RoU" id="0Y4-Pt-1Zh"/>
<outlet property="moreReactionsButton" destination="VQg-zY-j1F" id="ZfR-3o-lPV"/>
<outlet property="reactionsBackgroundView" destination="eXy-Kc-Ck7" id="VYi-YD-mb9"/>
<outlet property="reactionsStackView" destination="uzd-VB-mKT" id="DTV-Nh-bcm"/>
</connections>
<point key="canvasLocation" x="444.20289855072468" y="-718.19196428571422"/>
<point key="canvasLocation" x="296" y="-854"/>
</view>
</objects>
<resources>
<image name="more_reactions" width="22" height="20"/>
</resources>
</document>

View file

@ -20,4 +20,5 @@ import UIKit
enum ReactionsMenuViewAction {
case loadData
case tap(reaction: String)
case moreReactions
}

View file

@ -56,6 +56,8 @@ import Foundation
self.coordinatorDelegate?.reactionsMenuViewModel(self, didAddReaction: reaction, forEventId: self.eventId)
}
}
case .moreReactions:
self.coordinatorDelegate?.reactionsMenuViewModelDidTapMoreReactions(self, forEventId: self.eventId)
}
}

View file

@ -23,6 +23,7 @@ protocol ReactionsMenuViewModelViewDelegate: class {
@objc protocol ReactionsMenuViewModelCoordinatorDelegate: class {
func reactionsMenuViewModel(_ viewModel: ReactionsMenuViewModel, didAddReaction reaction: String, forEventId eventId: String)
func reactionsMenuViewModel(_ viewModel: ReactionsMenuViewModel, didRemoveReaction reaction: String, forEventId eventId: String)
func reactionsMenuViewModelDidTapMoreReactions(_ viewModel: ReactionsMenuViewModel, forEventId eventId: String)
}
protocol ReactionsMenuViewModelType {

View file

@ -201,6 +201,7 @@ final class RoomContextualMenuViewController: UIViewController, Themable {
func update(theme: Theme) {
self.menuToolbarView.update(theme: theme)
self.reactionsMenuView?.update(theme: theme)
}
// MARK: - Private
@ -225,6 +226,7 @@ final class RoomContextualMenuViewController: UIViewController, Themable {
if self.reactionsMenuContainerView.subviews.isEmpty {
let reactionsMenuView = ReactionsMenuView.loadFromNib()
self.reactionsMenuContainerView.vc_addSubViewMatchingParent(reactionsMenuView)
reactionsMenuView.update(theme: self.theme)
self.reactionsMenuView = reactionsMenuView
}

View file

@ -238,12 +238,12 @@
continue;
}
MXAggregatedReactions* reactions = cellData.reactions[componentEventId];
MXAggregatedReactions* reactions = cellData.reactions[componentEventId].aggregatedReactionsWithNonZeroCount;
BubbleReactionsView *reactionsView;
if (reactions && !isCollapsableCellCollapsed)
{
if (!component.event.isRedactedEvent && reactions && !isCollapsableCellCollapsed)
{
BOOL showAllReactions = [cellData showAllReactionsForEvent:componentEventId];
BubbleReactionsViewModel *bubbleReactionsViewModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:reactions
eventId:componentEventId
@ -607,4 +607,9 @@
}
}
- (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didLongPressForEventId:(NSString *)eventId
{
[self.delegate dataSource:self didRecognizeAction:kMXKRoomBubbleCellLongPressOnReactionView inCell:nil userInfo:@{ kMXKRoomBubbleCellEventIdKey: eventId }];
}
@end

View file

@ -65,9 +65,8 @@ final class EditHistoryCoordinatorBridgePresenter: NSObject {
let editHistoryCoordinator = EditHistoryCoordinator(session: self.session, formatter: formatter, event: self.event)
editHistoryCoordinator.delegate = self
let navigationController = RiotNavigationController()
let navigationController = RiotNavigationController(rootViewController: editHistoryCoordinator.toPresentable())
navigationController.modalPresentationStyle = .formSheet
navigationController.addChild(editHistoryCoordinator.toPresentable())
viewController.present(navigationController, animated: animated, completion: nil)
editHistoryCoordinator.start()

View file

@ -113,7 +113,7 @@ final class EditHistoryViewModel: EditHistoryViewModelType {
sself.nextBatch = response.nextBatch
sself.operation = nil
sself.process(editEvents: response.chunk,originalEvent: response.originalEvent, nextBatch: response.nextBatch)
sself.process(editEvents: response.chunk, originalEvent: response.originalEvent, nextBatch: response.nextBatch)
}, failure: { [weak self] error in
guard let sself = self else {

View file

@ -0,0 +1,83 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
extension EmojiItem: Decodable {
/// JSON keys associated to EmojiItem properties.
/// See https://github.com/missive/emoji-mart/blob/master/src/utils/data.js for minified letters informations.
///
/// - shortName: The commonly-agreed short name for the emoji, as supported in GitHub and others via the :short_name: syntax.
/// - name: The offical Unicode name.
/// - codepoint: The Unicode codepoint, as 4-5 hex digits. Where an emoji needs 2 or more codepoints, they are specified like 1F1EA-1F1F8.
/// - shortNames: An array of all the other known short names.
/// - keywords: Associated emoji keywords.
enum CodingKeys: String, CodingKey {
case shortName
case name = "a"
case codepoint = "b"
case shortNames = "n"
case keywords = "j"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let shortName = decoder.codingPath.last?.stringValue else {
throw DecodingError.dataCorruptedError(forKey: .shortName, in: container, debugDescription: "Cannot initialize short name")
}
let emojiUnicodeStringValue = try container.decode(String.self, forKey: .codepoint)
let unicodeStringComponents = emojiUnicodeStringValue.components(separatedBy: "-")
var emoji = ""
for unicodeStringComponent in unicodeStringComponents {
if let unicodeCodePoint = Int(unicodeStringComponent, radix: 16),
let emojiUnicodeScalar = UnicodeScalar(unicodeCodePoint) {
emoji.append(String(emojiUnicodeScalar))
} else {
throw DecodingError.dataCorruptedError(forKey: .codepoint, in: container, debugDescription: "Cannot initialize emoji")
}
}
let name = try container.decode(String.self, forKey: .name)
let shortNames: [String]
if let decodedShortNames = try container.decodeIfPresent([String].self, forKey: .shortNames) {
shortNames = decodedShortNames
} else {
shortNames = []
}
let keywords: [String]
if let decodedKeywords = try container.decodeIfPresent([String].self, forKey: .keywords) {
keywords = decodedKeywords
} else {
keywords = []
}
self.init(shortName: shortName,
value: emoji,
name: name,
shortNames: shortNames,
keywords: keywords)
}
}

View file

@ -0,0 +1,67 @@
/*
Copyright 2019 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 EmojiMartStore {
let categories: [EmojiJSONCategory]
let emojis: [EmojiItem]
}
// MARK: - Decodable
extension EmojiMartStore: Decodable {
/// JSON keys associated to EmojiJSONStore properties.
enum CodingKeys: String, CodingKey {
case categories
case emojis
}
/// JSON key associated to emoji short name.
struct EmojiKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let emojisContainer = try container.nestedContainer(keyedBy: EmojiKey.self, forKey: .emojis)
let emojis: [EmojiItem] = emojisContainer.allKeys.compactMap { (emojiKey) -> EmojiItem? in
let emojiItem: EmojiItem?
do {
emojiItem = try emojisContainer.decode(EmojiItem.self, forKey: emojiKey)
} catch {
print("[EmojiJSONStore] init(from decoder: Decoder) failed to parse emojiItem \(emojiKey) with error: \(error)")
emojiItem = nil
}
return emojiItem
}
let categories = try container.decode([EmojiJSONCategory].self, forKey: .categories)
self.init(categories: categories, emojis: emojis)
}
}

View file

@ -0,0 +1,40 @@
/*
Copyright 2019 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 EmojiMartCategory {
/// Emoji category identifier (e.g. "people")
let identifier: String
/// Emoji category name in english (e.g. "Smiley & People")
let name: String
/// List of emoji short names associated to the category (e.g. "people")
let emojiShortNames: [String]
}
// MARK: - Decodable
extension EmojiMartCategory: Decodable {
/// JSON keys associated to EmojiJSONCategory properties.
enum CodingKeys: String, CodingKey {
case identifier = "id"
case name
case emojiShortNames = "emojis"
}
}

View file

@ -0,0 +1,72 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
enum EmojiServiceError: Error {
case emojiJSONFileNotFound
}
/// Emoji service powered by Emoji Mart data (https://github.com/missive/emoji-mart/)
final class EmojiMartService: EmojiServiceType {
// MARK: - Constants
/// Emoji data coming from https://github.com/missive/emoji-mart/blob/master/data/apple.json
private static let jsonFilename = "apple_emojis_data"
// MARK: - Properties
private let serializationService: SerializationServiceType = SerializationService()
private let serviceQueue = DispatchQueue(label: "\(type(of: EmojiMartService.self))")
// MARK: - Public
func getEmojiCategories(completion: @escaping (MXResponse<[EmojiCategory]>) -> Void) {
self.serviceQueue.async {
do {
let emojiJSONData = try self.getEmojisJSONData()
let emojiJSONStore: EmojiMartStore = try self.serializationService.deserialize(emojiJSONData)
let emojiCategories = self.emojiCategories(from: emojiJSONStore)
completion(MXResponse.success(emojiCategories))
} catch {
completion(MXResponse.failure(error))
}
}
}
// MARK: - Private
private func getEmojisJSONData() throws -> Data {
guard let jsonDataURL = Bundle.main.url(forResource: EmojiMartService.jsonFilename, withExtension: "json") else {
throw EmojiServiceError.emojiJSONFileNotFound
}
let jsonData = try Data(contentsOf: jsonDataURL)
return jsonData
}
private func emojiCategories(from emojiJSONStore: EmojiMartStore) -> [EmojiCategory] {
let allEmojiItems = emojiJSONStore.emojis
return emojiJSONStore.categories.map { (jsonCategory) -> EmojiCategory in
let emojiItems = jsonCategory.emojiShortNames.compactMap({ (shortName) -> EmojiItem? in
return allEmojiItems.first(where: { $0.shortName == shortName })
})
return EmojiCategory(identifier: jsonCategory.identifier, emojis: emojiItems)
}
}
}

View file

@ -0,0 +1,67 @@
/*
Copyright 2019 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 EmojiMartStore {
let categories: [EmojiMartCategory]
let emojis: [EmojiItem]
}
// MARK: - Decodable
extension EmojiMartStore: Decodable {
/// JSON keys associated to EmojiMartStore properties.
enum CodingKeys: String, CodingKey {
case categories
case emojis
}
/// JSON key associated to emoji short name.
struct EmojiKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let emojisContainer = try container.nestedContainer(keyedBy: EmojiKey.self, forKey: .emojis)
let emojis: [EmojiItem] = emojisContainer.allKeys.compactMap { (emojiKey) -> EmojiItem? in
let emojiItem: EmojiItem?
do {
emojiItem = try emojisContainer.decode(EmojiItem.self, forKey: emojiKey)
} catch {
print("[EmojiJSONStore] init(from decoder: Decoder) failed to parse emojiItem \(emojiKey) with error: \(error)")
emojiItem = nil
}
return emojiItem
}
let categories = try container.decode([EmojiMartCategory].self, forKey: .categories)
self.init(categories: categories, emojis: emojis)
}
}

View file

@ -0,0 +1,21 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol EmojiServiceType {
func getEmojiCategories(completion: @escaping (MXResponse<[EmojiCategory]>) -> Void)
}

View file

@ -0,0 +1,32 @@
/*
Copyright 2019 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 EmojiCategory {
/// Emoji category identifier (e.g. "people")
let identifier: String
/// Emoji list associated to category
let emojis: [EmojiItem]
/// Emoji category localized name
var name: String {
let categoryNameLocalizationKey = "emoji_picker_\(self.identifier)_category"
return VectorL10n.tr("Vector", categoryNameLocalizationKey)
}
}

View file

@ -0,0 +1,57 @@
/*
Copyright 2019 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 EmojiItem {
// MARK: - Properties
/// The commonly-agreed short name for the emoji, as supported in GitHub and others via the :short_name: syntax (e.g. "grinning" for 😀).
let shortName: String
/// The emoji string (e.g. 😀)
let value: String
/// The offical Unicode name (e.g. "Grinning Face" for 😀)
let name: String
/// An array of all the other known short names (e.g. ["running"] for 🏃).
let shortNames: [String]
/// Associated emoji keywords (e.g. ["face","smile","happy"] for 😀).
let keywords: [String]
/// For emoji with multiple skin tone variations, a list of alternative emoji items.
let variations: [EmojiItem]
// MARK: - Setup
init(shortName: String,
value: String,
name: String,
shortNames: [String] = [],
keywords: [String] = [],
variations: [EmojiItem] = []) {
self.shortName = shortName
self.value = value
self.name = name
self.shortNames = shortNames
self.keywords = keywords
self.variations = variations
}
}

View file

@ -0,0 +1,69 @@
/*
Copyright 2019 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
final class EmojiStore {
static let shared = EmojiStore()
// MARK: - Properties
private var emojiCategories: [EmojiCategory] = []
// MARK: - Public
func getAll() -> [EmojiCategory] {
return self.emojiCategories
}
func set(_ emojiCategories: [EmojiCategory]) {
self.emojiCategories = emojiCategories
}
func findEmojiItemsSortedByCategory(with searchText: String) -> [EmojiCategory] {
let initial: [EmojiCategory] = []
let filteredEmojiCategories = emojiCategories.reduce(into: initial) { (filteredEmojiCategories, emojiCategory) in
let filteredEmojiItems = emojiCategory.emojis.filter({ (emojiItem) -> Bool in
// Do not use `String.localizedCaseInsensitiveContains` here as EmojiItem data is not localized for the moment
if emojiItem.name.vc_caseInsensitiveContains(searchText) {
return true
}
if emojiItem.keywords.contains(where: { $0.vc_caseInsensitiveContains(searchText) }) {
return true
}
let shortNamesMatch = emojiItem.shortNames.contains { text -> Bool in
return text.vc_caseInsensitiveContains(searchText)
}
return shortNamesMatch
})
if filteredEmojiItems.isEmpty == false {
let filteredEmojiCategory = EmojiCategory(identifier: emojiCategory.identifier, emojis: filteredEmojiItems)
filteredEmojiCategories.append(filteredEmojiCategory)
}
}
return filteredEmojiCategories
}
}

View file

@ -0,0 +1,23 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
struct EmojiPickerCategoryViewData {
let identifier: String
let name: String
let emojiViewDataList: [EmojiPickerItemViewData]
}

View file

@ -0,0 +1,76 @@
// File created from ScreenTemplate
// $ createScreen.sh toto EmojiPicker
/*
Copyright 2019 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
final class EmojiPickerCoordinator: EmojiPickerCoordinatorType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomId: String
private let eventId: String
private let router: NavigationRouter
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: EmojiPickerCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, roomId: String, eventId: String) {
self.session = session
self.roomId = roomId
self.eventId = eventId
self.router = NavigationRouter(navigationController: RiotNavigationController())
}
// MARK: - Public methods
func start() {
let emojiPickerViewModel = EmojiPickerViewModel(session: self.session, roomId: self.roomId, eventId: self.eventId)
let emojiPickerViewController = EmojiPickerViewController.instantiate(with: emojiPickerViewModel)
emojiPickerViewModel.coordinatorDelegate = self
self.router.setRootModule(emojiPickerViewController)
}
func toPresentable() -> UIViewController {
return self.router.toPresentable()
}
}
// MARK: - EmojiPickerViewModelCoordinatorDelegate
extension EmojiPickerCoordinator: EmojiPickerViewModelCoordinatorDelegate {
func emojiPickerViewModel(_ viewModel: EmojiPickerViewModelType, didAddEmoji emoji: String, forEventId eventId: String) {
self.delegate?.emojiPickerCoordinator(self, didAddEmoji: emoji, forEventId: eventId)
}
func emojiPickerViewModel(_ viewModel: EmojiPickerViewModelType, didRemoveEmoji emoji: String, forEventId eventId: String) {
self.delegate?.emojiPickerCoordinator(self, didRemoveEmoji: emoji, forEventId: eventId)
}
func emojiPickerViewModelDidCancel(_ viewModel: EmojiPickerViewModelType) {
self.delegate?.emojiPickerCoordinatorDidCancel(self)
}
}

View file

@ -0,0 +1,117 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
@objc protocol EmojiPickerCoordinatorBridgePresenterDelegate {
func emojiPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: EmojiPickerCoordinatorBridgePresenter, didAddEmoji emoji: String, forEventId eventId: String)
func emojiPickerCoordinatorBridgePresenter(_ coordinatorBridgePresenter: EmojiPickerCoordinatorBridgePresenter, didRemoveEmoji emoji: String, forEventId eventId: String)
func emojiPickerCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: EmojiPickerCoordinatorBridgePresenter)
}
/// EmojiPickerCoordinatorBridgePresenter enables to start EmojiPickerCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
@objcMembers
final class EmojiPickerCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomId: String
private let eventId: String
private var coordinator: EmojiPickerCoordinator?
// MARK: Public
weak var delegate: EmojiPickerCoordinatorBridgePresenterDelegate?
// MARK: - Setup
init(session: MXSession, roomId: String, eventId: String) {
self.session = session
self.roomId = roomId
self.eventId = eventId
super.init()
}
// MARK: - Public
func present(from viewController: UIViewController,
sourceView: UIView?,
sourceRect: CGRect,
animated: Bool) {
let emojiPickerCoordinator = EmojiPickerCoordinator(session: self.session, roomId: self.roomId, eventId: self.eventId)
emojiPickerCoordinator.delegate = self
let emojiPickerPresentable = emojiPickerCoordinator.toPresentable()
if let sourceView = sourceView {
emojiPickerPresentable.modalPresentationStyle = .popover
if let popoverPresentationController = emojiPickerPresentable.popoverPresentationController {
popoverPresentationController.sourceView = sourceView
let finalSourceRect: CGRect
if sourceRect != CGRect.null {
finalSourceRect = sourceRect
} else {
finalSourceRect = sourceView.bounds
}
popoverPresentationController.sourceRect = finalSourceRect
}
}
viewController.present(emojiPickerPresentable, animated: animated, completion: nil)
emojiPickerCoordinator.start()
self.coordinator = emojiPickerCoordinator
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
if let completion = completion {
completion()
}
}
}
}
// MARK: - EmojiPickerCoordinatorDelegate
extension EmojiPickerCoordinatorBridgePresenter: EmojiPickerCoordinatorDelegate {
func emojiPickerCoordinator(_ coordinator: EmojiPickerCoordinatorType, didAddEmoji emoji: String, forEventId eventId: String) {
self.delegate?.emojiPickerCoordinatorBridgePresenter(self, didAddEmoji: emoji, forEventId: eventId)
}
func emojiPickerCoordinator(_ coordinator: EmojiPickerCoordinatorType, didRemoveEmoji emoji: String, forEventId eventId: String) {
self.delegate?.emojiPickerCoordinatorBridgePresenter(self, didRemoveEmoji: emoji, forEventId: eventId)
}
func emojiPickerCoordinatorDidCancel(_ coordinator: EmojiPickerCoordinatorType) {
self.delegate?.emojiPickerCoordinatorBridgePresenterDidCancel(self)
}
}

View file

@ -0,0 +1,30 @@
// File created from ScreenTemplate
// $ createScreen.sh toto EmojiPicker
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol EmojiPickerCoordinatorDelegate: class {
func emojiPickerCoordinator(_ coordinator: EmojiPickerCoordinatorType, didAddEmoji emoji: String, forEventId eventId: String)
func emojiPickerCoordinator(_ coordinator: EmojiPickerCoordinatorType, didRemoveEmoji emoji: String, forEventId eventId: String)
func emojiPickerCoordinatorDidCancel(_ coordinator: EmojiPickerCoordinatorType)
}
/// `EmojiPickerCoordinatorType` is a protocol describing a Coordinator that handle emoji picker navigation flow.
protocol EmojiPickerCoordinatorType: Coordinator, Presentable {
var delegate: EmojiPickerCoordinatorDelegate? { get }
}

View file

@ -0,0 +1,36 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
import Reusable
final class EmojiPickerHeaderView: UICollectionReusableView, NibReusable {
// MARK: - Properties
@IBOutlet private weak var titleLabel: UILabel!
// MARK: - Public
func update(theme: Theme) {
self.backgroundColor = theme.backgroundColor
self.titleLabel.textColor = theme.headerTextPrimaryColor
}
func fill(with title: String) {
titleLabel.text = title
}
}

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="EmojiPickerHeaderView" id="U6b-Vx-4bR" customClass="EmojiPickerHeaderView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wrW-Gv-YJV">
<rect key="frame" x="20" y="8" width="280" height="34"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="wrW-Gv-YJV" secondAttribute="bottom" constant="8" id="D7a-Br-Vlz"/>
<constraint firstItem="wrW-Gv-YJV" firstAttribute="top" secondItem="U6b-Vx-4bR" secondAttribute="top" constant="8" id="IGK-ta-CnU"/>
<constraint firstItem="wrW-Gv-YJV" firstAttribute="leading" secondItem="VXr-Tz-HHm" secondAttribute="leading" constant="20" id="T3V-4C-3Qu"/>
<constraint firstItem="VXr-Tz-HHm" firstAttribute="trailing" secondItem="wrW-Gv-YJV" secondAttribute="trailing" constant="20" id="wSZ-15-Co2"/>
</constraints>
<viewLayoutGuide key="safeArea" id="VXr-Tz-HHm"/>
<connections>
<outlet property="titleLabel" destination="wrW-Gv-YJV" id="wYw-xg-hha"/>
</connections>
<point key="canvasLocation" x="-404" y="-81"/>
</collectionReusableView>
</objects>
</document>

View file

@ -0,0 +1,23 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
struct EmojiPickerItemViewData {
let identifier: String
let emoji: String
let isSelected: Bool
}

View file

@ -0,0 +1,27 @@
// File created from ScreenTemplate
// $ createScreen.sh toto EmojiPicker
/*
Copyright 2019 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
/// EmojiPickerViewController view actions exposed to view model
enum EmojiPickerViewAction {
case loadData
case cancel
case tap(emojiItemViewData: EmojiPickerItemViewData)
case search(text: String?)
}

View file

@ -0,0 +1,101 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
import Reusable
final class EmojiPickerViewCell: UICollectionViewCell, NibReusable, Themable {
// MARK: - Constants
private enum Constants {
static let selectedBorderWidth: CGFloat = 1.0
static let selectedBackgroundRadius: CGFloat = 5.0
static let emojiHighlightedAlpha: CGFloat = 0.3
}
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var reactionBackgroundView: UIView!
@IBOutlet private weak var emojiLabel: UILabel!
// MARK: Private
private var theme: Theme?
// MARK: Public
private var isReactionSelected: Bool = false
override var isHighlighted: Bool {
didSet {
self.emojiLabel.alpha = isHighlighted ? Constants.emojiHighlightedAlpha : 1.0
}
}
// MARK: - Life cycle
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.reactionBackgroundView.layer.masksToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
self.reactionBackgroundView.layer.cornerRadius = self.reactionBackgroundView.frame.width/4.0
}
// MARK: - Public
func fill(viewData: EmojiPickerItemViewData) {
self.emojiLabel.text = viewData.emoji
self.isReactionSelected = viewData.isSelected
self.updateViews()
}
func update(theme: Theme) {
self.theme = theme
self.reactionBackgroundView.layer.borderColor = theme.tintColor.cgColor
self.emojiLabel.textColor = theme.textPrimaryColor
self.updateViews()
}
// MARK: - Private
private func updateViews() {
let reactionBackgroundColor: UIColor?
let reactionBackgroundBorderWidth: CGFloat
if self.isReactionSelected {
reactionBackgroundColor = self.theme?.tintBackgroundColor
reactionBackgroundBorderWidth = Constants.selectedBorderWidth
} else {
reactionBackgroundColor = self.theme?.headerBackgroundColor
reactionBackgroundBorderWidth = 0.0
}
self.reactionBackgroundView.layer.borderWidth = reactionBackgroundBorderWidth
self.reactionBackgroundView.backgroundColor = reactionBackgroundColor
}
}

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="EmojiPickerViewCell" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Sam-a3-I0s">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="👍" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9GU-d6-Cnv">
<rect key="frame" x="6" y="4" width="38" height="42"/>
<fontDescription key="fontDescription" type="system" pointSize="30"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="0.95294117649999999" green="0.97254901959999995" blue="0.99215686270000003" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstItem="9GU-d6-Cnv" firstAttribute="top" secondItem="Sam-a3-I0s" secondAttribute="top" constant="4" id="T08-V8-wu1"/>
<constraint firstAttribute="bottom" secondItem="9GU-d6-Cnv" secondAttribute="bottom" constant="4" id="dgR-Un-bnz"/>
<constraint firstAttribute="trailing" secondItem="9GU-d6-Cnv" secondAttribute="trailing" constant="6" id="gnQ-6e-N8Y"/>
<constraint firstItem="9GU-d6-Cnv" firstAttribute="leading" secondItem="Sam-a3-I0s" secondAttribute="leading" constant="6" id="wHy-AN-9GI"/>
</constraints>
</view>
</subviews>
</view>
<constraints>
<constraint firstAttribute="trailing" secondItem="Sam-a3-I0s" secondAttribute="trailing" id="5FI-St-rx2"/>
<constraint firstItem="Sam-a3-I0s" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="Oge-ZF-LX9"/>
<constraint firstAttribute="bottom" secondItem="Sam-a3-I0s" secondAttribute="bottom" id="f0f-GW-KI4"/>
<constraint firstItem="Sam-a3-I0s" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="vze-ak-cqC"/>
</constraints>
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
<connections>
<outlet property="emojiLabel" destination="9GU-d6-Cnv" id="5kc-lg-TpL"/>
<outlet property="reactionBackgroundView" destination="Sam-a3-I0s" id="x8I-E5-BRG"/>
</connections>
</collectionViewCell>
</objects>
</document>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="V8j-Lb-PgC">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Emoji Picker View Controller-->
<scene sceneID="mt5-wz-YKA">
<objects>
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="V8j-Lb-PgC" customClass="EmojiPickerViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="EL9-GA-lwo">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="Kjj-O1-UsZ">
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="iPs-P1-bkS">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells/>
<connections>
<outlet property="dataSource" destination="V8j-Lb-PgC" id="ZkG-A0-JGC"/>
<outlet property="delegate" destination="V8j-Lb-PgC" id="iXC-RG-INF"/>
</connections>
</collectionView>
</subviews>
<color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstItem="Kjj-O1-UsZ" firstAttribute="leading" secondItem="EL9-GA-lwo" secondAttribute="leading" id="6Ys-O9-EZy"/>
<constraint firstItem="bFg-jh-JZB" firstAttribute="top" secondItem="Kjj-O1-UsZ" secondAttribute="top" id="MQy-0K-KQf"/>
<constraint firstAttribute="trailing" secondItem="Kjj-O1-UsZ" secondAttribute="trailing" id="XkY-kj-afK"/>
<constraint firstAttribute="bottom" secondItem="Kjj-O1-UsZ" secondAttribute="bottom" id="cdk-7X-2Pn"/>
</constraints>
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
</view>
<extendedEdge key="edgesForExtendedLayout" bottom="YES"/>
<connections>
<outlet property="collectionView" destination="Kjj-O1-UsZ" id="iBb-J9-0Uc"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zK0-v6-7Wt" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-3199.1999999999998" y="-647.22638680659679"/>
</scene>
</scenes>
</document>

View file

@ -0,0 +1,319 @@
// File created from ScreenTemplate
// $ createScreen.sh toto EmojiPicker
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
import Reusable
final class EmojiPickerViewController: UIViewController {
// MARK: - Constants
private enum CollectionViewLayout {
static let sectionInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
static let minimumInteritemSpacing: CGFloat = 6.0
static let minimumLineSpacing: CGFloat = 2.0
static let itemSize = CGSize(width: 50, height: 50)
}
private static let sizingHeaderView = EmojiPickerHeaderView.loadFromNib()
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var collectionView: UICollectionView!
// MARK: Private
private var viewModel: EmojiPickerViewModelType!
private var theme: Theme!
private var keyboardAvoider: KeyboardAvoider?
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private var searchController: UISearchController?
private var emojiCategories: [EmojiPickerCategoryViewData] = []
// MARK: - Setup
class func instantiate(with viewModel: EmojiPickerViewModelType) -> EmojiPickerViewController {
let viewController = StoryboardScene.EmojiPickerViewController.initialScene.instantiate()
viewController.viewModel = viewModel
viewController.theme = ThemeService.shared().theme
return viewController
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = VectorL10n.emojiPickerTitle
self.setupViews()
self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.collectionView)
self.activityPresenter = ActivityIndicatorPresenter()
self.errorPresenter = MXKErrorAlertPresentation()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
self.viewModel.viewDelegate = self
self.viewModel.process(viewAction: .loadData)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.keyboardAvoider?.startAvoiding()
// Update theme here otherwise the UISearchBar search text color is not updated
self.update(theme: self.theme)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Enable to hide search bar on scrolling after first time view appear
if #available(iOS 11.0, *) {
self.navigationItem.hidesSearchBarWhenScrolling = true
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.keyboardAvoider?.stopAvoiding()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.theme.statusBarStyle
}
// MARK: - Private
private func update(theme: Theme) {
self.theme = theme
self.view.backgroundColor = theme.headerBackgroundColor
if let navigationBar = self.navigationController?.navigationBar {
theme.applyStyle(onNavigationBar: navigationBar)
}
if let searchController = self.searchController {
theme.applyStyle(onSearchBar: searchController.searchBar)
}
}
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@objc private func themeDidChange() {
self.update(theme: ThemeService.shared().theme)
}
private func setupViews() {
let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
self?.cancelButtonAction()
}
self.navigationItem.rightBarButtonItem = cancelBarButtonItem
self.setupCollectionView()
if #available(iOS 11.0, *) {
self.setupSearchController()
}
}
private func setupCollectionView() {
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.keyboardDismissMode = .interactive
if let collectionViewFlowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
collectionViewFlowLayout.minimumInteritemSpacing = CollectionViewLayout.minimumInteritemSpacing
collectionViewFlowLayout.minimumLineSpacing = CollectionViewLayout.minimumLineSpacing
collectionViewFlowLayout.itemSize = CollectionViewLayout.itemSize
collectionViewFlowLayout.sectionInset = CollectionViewLayout.sectionInsets
collectionViewFlowLayout.sectionHeadersPinToVisibleBounds = true // Enable sticky headers
// Avoid device notch in landascape (e.g. iPhone X)
if #available(iOS 11.0, *) {
collectionViewFlowLayout.sectionInsetReference = .fromSafeArea
}
}
self.collectionView.register(supplementaryViewType: EmojiPickerHeaderView.self, ofKind: UICollectionView.elementKindSectionHeader)
self.collectionView.register(cellType: EmojiPickerViewCell.self)
}
private func setupSearchController() {
let searchController = UISearchController(searchResultsController: nil)
searchController.dimsBackgroundDuringPresentation = false
searchController.searchResultsUpdater = self
searchController.searchBar.placeholder = VectorL10n.searchDefaultPlaceholder
searchController.hidesNavigationBarDuringPresentation = false
if #available(iOS 11.0, *) {
self.navigationItem.searchController = searchController
// Make the search bar visible on first view appearance
self.navigationItem.hidesSearchBarWhenScrolling = false
}
self.definesPresentationContext = true
self.searchController = searchController
}
private func render(viewState: EmojiPickerViewState) {
switch viewState {
case .loading:
self.renderLoading()
case .loaded(emojiCategories: let emojiCategories):
self.renderLoaded(emojiCategories: emojiCategories)
case .error(let error):
self.render(error: error)
}
}
private func renderLoading() {
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
}
private func renderLoaded(emojiCategories: [EmojiPickerCategoryViewData]) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.update(emojiCategories: emojiCategories)
}
private func render(error: Error) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
}
private func update(emojiCategories: [EmojiPickerCategoryViewData]) {
self.emojiCategories = emojiCategories
self.collectionView.reloadData()
}
private func emojiItemViewData(at indexPath: IndexPath) -> EmojiPickerItemViewData {
return self.emojiCategories[indexPath.section].emojiViewDataList[indexPath.row]
}
private func emojiCategoryViewData(at section: Int) -> EmojiPickerCategoryViewData? {
return self.emojiCategories[section]
}
private func headerViewSize(for title: String) -> CGSize {
let sizingHeaderView = EmojiPickerViewController.sizingHeaderView
sizingHeaderView.fill(with: title)
sizingHeaderView.setNeedsLayout()
sizingHeaderView.layoutIfNeeded()
var fittingSize = UIView.layoutFittingCompressedSize
fittingSize.width = self.collectionView.bounds.size.width
return sizingHeaderView.systemLayoutSizeFitting(fittingSize)
}
// MARK: - Actions
private func cancelButtonAction() {
self.viewModel.process(viewAction: .cancel)
}
}
// MARK: - UICollectionViewDataSource
extension EmojiPickerViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return self.emojiCategories.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.emojiCategories[section].emojiViewDataList.count
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let emojiPickerCategory = self.emojiCategories[indexPath.section]
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, for: indexPath) as EmojiPickerHeaderView
headerView.update(theme: self.theme)
headerView.fill(with: emojiPickerCategory.name)
return headerView
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: EmojiPickerViewCell = collectionView.dequeueReusableCell(for: indexPath)
if let theme = self.theme {
cell.update(theme: theme)
}
let viewData = self.emojiItemViewData(at: indexPath)
cell.fill(viewData: viewData)
return cell
}
}
// MARK: - UICollectionViewDelegate
extension EmojiPickerViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let emojiItemViewData = self.emojiItemViewData(at: indexPath)
self.viewModel.process(viewAction: .tap(emojiItemViewData: emojiItemViewData))
}
func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {
// Fix UICollectionView scroll bar appears underneath header view
view.layer.zPosition = 0.0
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension EmojiPickerViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let emojiCategory = self.emojiCategories[section]
let headerSize = self.headerViewSize(for: emojiCategory.name)
return headerSize
}
}
// MARK: - EmojiPickerViewModelViewDelegate
extension EmojiPickerViewController: EmojiPickerViewModelViewDelegate {
func emojiPickerViewModel(_ viewModel: EmojiPickerViewModelType, didUpdateViewState viewSate: EmojiPickerViewState) {
self.render(viewState: viewSate)
}
}
// MARK: - UISearchResultsUpdating
extension EmojiPickerViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
let searchText = searchController.searchBar.text
self.viewModel.process(viewAction: .search(text: searchText))
}
}

View file

@ -0,0 +1,153 @@
// File created from ScreenTemplate
// $ createScreen.sh toto EmojiPicker
/*
Copyright 2019 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
final class EmojiPickerViewModel: EmojiPickerViewModelType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomId: String
private let eventId: String
private let emojiService: EmojiServiceType
private let emojiStore: EmojiStore
private let processingQueue: DispatchQueue
private lazy var aggregatedReactionsByEmoji: [String: MXReactionCount] = {
return self.buildAggregatedReactionsByEmoji()
}()
// MARK: Public
weak var viewDelegate: EmojiPickerViewModelViewDelegate?
weak var coordinatorDelegate: EmojiPickerViewModelCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, roomId: String, eventId: String) {
self.session = session
self.roomId = roomId
self.eventId = eventId
self.emojiService = EmojiMartService()
self.emojiStore = EmojiStore.shared
self.processingQueue = DispatchQueue(label: "\(type(of: self))")
}
// MARK: - Public
func process(viewAction: EmojiPickerViewAction) {
switch viewAction {
case .loadData:
self.loadData()
case .tap(emojiItemViewData: let emojiItemViewData):
let emoji = emojiItemViewData.emoji
if emojiItemViewData.isSelected {
self.coordinatorDelegate?.emojiPickerViewModel(self, didRemoveEmoji: emoji, forEventId: self.eventId)
} else {
self.coordinatorDelegate?.emojiPickerViewModel(self, didAddEmoji: emoji, forEventId: self.eventId)
}
case .search(text: let searchText):
self.searchEmojis(with: searchText)
case .cancel:
self.coordinatorDelegate?.emojiPickerViewModelDidCancel(self)
}
}
// MARK: - Private
private func loadData() {
if self.emojiStore.getAll().isEmpty == false {
let emojiCategories = self.emojiStore.getAll()
let emojiCatagoryViewDataList = self.emojiCatagoryViewDataList(from: emojiCategories)
self.update(viewState: .loaded(emojiCategories: emojiCatagoryViewDataList))
} else {
self.update(viewState: .loading)
self.emojiService.getEmojiCategories { response in
switch response {
case .success(let emojiCategories):
self.emojiStore.set(emojiCategories)
let emojiCatagoryViewDataList = self.emojiCatagoryViewDataList(from: emojiCategories)
DispatchQueue.main.async {
self.update(viewState: .loaded(emojiCategories: emojiCatagoryViewDataList))
}
case .failure(let error):
DispatchQueue.main.async {
self.update(viewState: .error(error))
}
}
}
}
}
private func searchEmojis(with searchText: String?) {
self.processingQueue.async {
let filteredEmojiCategories: [EmojiCategory]
if let searchText = searchText, searchText.isEmpty == false {
filteredEmojiCategories = self.emojiStore.findEmojiItemsSortedByCategory(with: searchText)
} else {
filteredEmojiCategories = self.emojiStore.getAll()
}
let emojiCatagoryViewDataList = self.emojiCatagoryViewDataList(from: filteredEmojiCategories)
DispatchQueue.main.async {
self.update(viewState: .loaded(emojiCategories: emojiCatagoryViewDataList))
}
}
}
private func update(viewState: EmojiPickerViewState) {
self.viewDelegate?.emojiPickerViewModel(self, didUpdateViewState: viewState)
}
private func emojiCatagoryViewDataList(from emojiCategories: [EmojiCategory]) -> [EmojiPickerCategoryViewData] {
return emojiCategories.map { (emojiCategory) -> EmojiPickerCategoryViewData in
let emojiPickerViewDataList = emojiCategory.emojis.map({ (emojiItem) -> EmojiPickerItemViewData in
let isSelected = self.isUserReacted(with: emojiItem.value)
return EmojiPickerItemViewData(identifier: emojiItem.shortName, emoji: emojiItem.value, isSelected: isSelected)
})
return EmojiPickerCategoryViewData(identifier: emojiCategory.identifier, name: emojiCategory.name, emojiViewDataList: emojiPickerViewDataList)
}
}
private func isUserReacted(with emoji: String) -> Bool {
guard let reactionCount = self.aggregatedReactionsByEmoji[emoji] else {
return false
}
return reactionCount.myUserHasReacted
}
private func buildAggregatedReactionsByEmoji() -> [String: MXReactionCount] {
guard let aggregatedReactions = self.session.aggregations.aggregatedReactions(onEvent: self.eventId, inRoom: self.roomId) else {
return [:]
}
let initial: [String: MXReactionCount] = [:]
return aggregatedReactions.reactions.reduce(into: initial) { (aggregatedReactionsByEmoji, reactionCount) in
aggregatedReactionsByEmoji[reactionCount.reaction] = reactionCount
}
}
}

View file

@ -0,0 +1,38 @@
// File created from ScreenTemplate
// $ createScreen.sh toto EmojiPicker
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol EmojiPickerViewModelViewDelegate: class {
func emojiPickerViewModel(_ viewModel: EmojiPickerViewModelType, didUpdateViewState viewSate: EmojiPickerViewState)
}
protocol EmojiPickerViewModelCoordinatorDelegate: class {
func emojiPickerViewModel(_ viewModel: EmojiPickerViewModelType, didAddEmoji emoji: String, forEventId eventId: String)
func emojiPickerViewModel(_ viewModel: EmojiPickerViewModelType, didRemoveEmoji emoji: String, forEventId eventId: String)
func emojiPickerViewModelDidCancel(_ viewModel: EmojiPickerViewModelType)
}
/// Protocol describing the view model used by `EmojiPickerViewController`
protocol EmojiPickerViewModelType {
var viewDelegate: EmojiPickerViewModelViewDelegate? { get set }
var coordinatorDelegate: EmojiPickerViewModelCoordinatorDelegate? { get set }
func process(viewAction: EmojiPickerViewAction)
}

View file

@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh toto EmojiPicker
/*
Copyright 2019 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
/// EmojiPickerViewController view state
enum EmojiPickerViewState {
case loading
case loaded(emojiCategories: [EmojiPickerCategoryViewData])
case error(Error)
}

View file

@ -1691,7 +1691,7 @@
// FIXME: this all seems incredibly fragile and tied to gutwrenching the current UISearchBar internals.
// text color
UITextField *searchBarTextField = [searchBar valueForKey:@"_searchField"];
UITextField *searchBarTextField = searchBar.vc_searchTextField;
searchBarTextField.textColor = ThemeService.shared.theme.textSecondaryColor;
// Magnifying glass icon.

View file

@ -0,0 +1,89 @@
/*
Copyright 2019 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
@objc protocol ReactionHistoryCoordinatorBridgePresenterDelegate {
func reactionHistoryCoordinatorBridgePresenterDelegateDidClose(_ coordinatorBridgePresenter: ReactionHistoryCoordinatorBridgePresenter)
}
/// ReactionHistoryCoordinatorBridgePresenter enables to start ReactionHistoryCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
@objcMembers
final class ReactionHistoryCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomId: String
private let eventId: String
private var coordinator: ReactionHistoryCoordinator?
// MARK: Public
var isPresenting: Bool {
return self.coordinator != nil
}
weak var delegate: ReactionHistoryCoordinatorBridgePresenterDelegate?
// MARK: - Setup
init(session: MXSession, roomId: String, eventId: String) {
self.session = session
self.roomId = roomId
self.eventId = eventId
super.init()
}
// MARK: - Public
func present(from viewController: UIViewController, animated: Bool) {
let reactionHistoryCoordinator = ReactionHistoryCoordinator(session: self.session, roomId: self.roomId, eventId: self.eventId)
reactionHistoryCoordinator.delegate = self
let coordinatorPresentable = reactionHistoryCoordinator.toPresentable()
coordinatorPresentable.modalPresentationStyle = .formSheet
viewController.present(coordinatorPresentable, animated: animated, completion: nil)
reactionHistoryCoordinator.start()
self.coordinator = reactionHistoryCoordinator
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
if let completion = completion {
completion()
}
}
}
}
// MARK: - ReactionHistoryCoordinatorDelegate
extension ReactionHistoryCoordinatorBridgePresenter: ReactionHistoryCoordinatorDelegate {
func reactionHistoryCoordinatorDidClose(_ coordinator: ReactionHistoryCoordinatorType) {
self.delegate?.reactionHistoryCoordinatorBridgePresenterDelegateDidClose(self)
}
}

View file

@ -0,0 +1,68 @@
// File created from ScreenTemplate
// $ createScreen.sh ReactionHistory ReactionHistory
/*
Copyright 2019 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
final class ReactionHistoryCoordinator: ReactionHistoryCoordinatorType {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomId: String
private let eventId: String
private let router: NavigationRouter
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
weak var delegate: ReactionHistoryCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, roomId: String, eventId: String) {
self.session = session
self.roomId = roomId
self.eventId = eventId
self.router = NavigationRouter(navigationController: RiotNavigationController())
}
// MARK: - Public methods
func start() {
let reactionHistoryViewModel = ReactionHistoryViewModel(session: session, roomId: roomId, eventId: eventId)
let reactionHistoryViewController = ReactionHistoryViewController.instantiate(with: reactionHistoryViewModel)
reactionHistoryViewModel.coordinatorDelegate = self
self.router.setRootModule(reactionHistoryViewController)
}
func toPresentable() -> UIViewController {
return self.router.toPresentable()
}
}
// MARK: - ReactionHistoryViewModelCoordinatorDelegate
extension ReactionHistoryCoordinator: ReactionHistoryViewModelCoordinatorDelegate {
func reactionHistoryViewModelDidClose(_ viewModel: ReactionHistoryViewModelType) {
self.delegate?.reactionHistoryCoordinatorDidClose(self)
}
}

View file

@ -0,0 +1,28 @@
// File created from ScreenTemplate
// $ createScreen.sh ReactionHistory ReactionHistory
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol ReactionHistoryCoordinatorDelegate: class {
func reactionHistoryCoordinatorDidClose(_ coordinator: ReactionHistoryCoordinatorType)
}
/// `ReactionHistoryCoordinatorType` is a protocol describing a Coordinator that handle reaction history navigation flow.
protocol ReactionHistoryCoordinatorType: Coordinator, Presentable {
var delegate: ReactionHistoryCoordinatorDelegate? { get }
}

View file

@ -0,0 +1,25 @@
// File created from ScreenTemplate
// $ createScreen.sh ReactionHistory ReactionHistory
/*
Copyright 2019 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
/// ReactionHistoryViewController view actions exposed to view model
enum ReactionHistoryViewAction {
case loadMore
case close
}

View file

@ -0,0 +1,42 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
import Reusable
final class ReactionHistoryViewCell: UITableViewCell, NibReusable, Themable {
// MARK: - Properties
@IBOutlet private weak var reactionLabel: UILabel!
@IBOutlet private weak var userDisplayNameLabel: UILabel!
@IBOutlet private weak var timestampLabel: UILabel!
// MARK: - Public
func fill(with viewData: ReactionHistoryViewData) {
self.reactionLabel.text = viewData.reaction
self.userDisplayNameLabel.text = viewData.userDisplayName
self.timestampLabel.text = viewData.dateString
}
func update(theme: Theme) {
self.backgroundColor = theme.backgroundColor
self.reactionLabel.textColor = theme.textPrimaryColor
self.userDisplayNameLabel.textColor = theme.textPrimaryColor
self.timestampLabel.textColor = theme.textSecondaryColor
}
}

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="46" id="KGk-i7-Jjw" customClass="ReactionHistoryViewCell" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="578" height="46"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="578" height="45.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" horizontalCompressionResistancePriority="752" text="😀" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sgX-dJ-kCR">
<rect key="frame" x="20" y="10" width="24" height="25.5"/>
<fontDescription key="fontDescription" type="system" pointSize="18"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="user name" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Vcy-eW-DCg">
<rect key="frame" x="54" y="13" width="460.5" height="19.5"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="20:30" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Euk-Mr-DGp">
<rect key="frame" x="524.5" y="15.5" width="33.5" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="sgX-dJ-kCR" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" priority="750" constant="10" id="1d4-Pd-ffp"/>
<constraint firstItem="Vcy-eW-DCg" firstAttribute="leading" secondItem="sgX-dJ-kCR" secondAttribute="trailing" constant="10" id="2Se-Si-LeY"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="Vcy-eW-DCg" secondAttribute="bottom" constant="10" id="AKa-IS-sS3"/>
<constraint firstAttribute="bottom" secondItem="sgX-dJ-kCR" secondAttribute="bottom" priority="750" constant="10" id="Bq1-yU-Eju"/>
<constraint firstItem="sgX-dJ-kCR" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="20" id="Gnp-Pg-a5X"/>
<constraint firstItem="Vcy-eW-DCg" firstAttribute="centerY" secondItem="sgX-dJ-kCR" secondAttribute="centerY" id="N2F-Pw-CRB"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="sgX-dJ-kCR" secondAttribute="bottom" constant="10" id="SQg-Iz-ZA6"/>
<constraint firstItem="Euk-Mr-DGp" firstAttribute="leading" secondItem="Vcy-eW-DCg" secondAttribute="trailing" constant="10" id="W4s-gm-wXT"/>
<constraint firstAttribute="trailing" secondItem="Euk-Mr-DGp" secondAttribute="trailing" constant="20" id="c1y-WQ-DmA"/>
<constraint firstItem="sgX-dJ-kCR" firstAttribute="top" relation="greaterThanOrEqual" secondItem="H2p-sc-9uM" secondAttribute="top" constant="10" id="k6s-g1-TzX"/>
<constraint firstItem="Vcy-eW-DCg" firstAttribute="top" relation="greaterThanOrEqual" secondItem="H2p-sc-9uM" secondAttribute="top" constant="10" id="qRt-N3-3rj"/>
<constraint firstItem="Euk-Mr-DGp" firstAttribute="centerY" secondItem="sgX-dJ-kCR" secondAttribute="centerY" id="zNz-zT-nFd"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
<connections>
<outlet property="reactionLabel" destination="sgX-dJ-kCR" id="gGv-pT-xAa"/>
<outlet property="timestampLabel" destination="Euk-Mr-DGp" id="ZCp-Og-Tar"/>
<outlet property="userDisplayNameLabel" destination="Vcy-eW-DCg" id="v95-in-PbT"/>
</connections>
<point key="canvasLocation" x="-71.014492753623188" y="79.017857142857139"/>
</tableViewCell>
</objects>
</document>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="V8j-Lb-PgC">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Reaction History View Controller-->
<scene sceneID="mt5-wz-YKA">
<objects>
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="V8j-Lb-PgC" customClass="ReactionHistoryViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="EL9-GA-lwo">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="HkR-sa-Jjv">
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<outlet property="dataSource" destination="V8j-Lb-PgC" id="NeJ-vG-i82"/>
<outlet property="delegate" destination="V8j-Lb-PgC" id="Ddg-k9-R6c"/>
</connections>
</tableView>
</subviews>
<color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/>
<constraints>
<constraint firstItem="HkR-sa-Jjv" firstAttribute="top" secondItem="bFg-jh-JZB" secondAttribute="top" id="8aH-El-E6A"/>
<constraint firstAttribute="trailing" secondItem="HkR-sa-Jjv" secondAttribute="trailing" id="L5p-CX-oPq"/>
<constraint firstItem="HkR-sa-Jjv" firstAttribute="leading" secondItem="EL9-GA-lwo" secondAttribute="leading" id="Vo4-SY-4qi"/>
<constraint firstAttribute="bottom" secondItem="HkR-sa-Jjv" secondAttribute="bottom" id="kdK-64-7jj"/>
</constraints>
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
</view>
<connections>
<outlet property="tableView" destination="HkR-sa-Jjv" id="PHl-xo-AGs"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zK0-v6-7Wt" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-3198" y="-647"/>
</scene>
</scenes>
</document>

View file

@ -0,0 +1,206 @@
// File created from ScreenTemplate
// $ createScreen.sh ReactionHistory ReactionHistory
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
final class ReactionHistoryViewController: UIViewController {
// MARK: - Constants
private enum TableView {
static let estimatedRowHeight: CGFloat = 21.0
static let contentInset = UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0)
}
// MARK: - Properties
// MARK: Outlets
@IBOutlet private weak var tableView: UITableView!
// MARK: Private
private var viewModel: ReactionHistoryViewModelType!
private var theme: Theme!
private var errorPresenter: MXKErrorPresentation!
private var activityPresenter: ActivityIndicatorPresenter!
private var isViewAppearedOnce: Bool = false
private var reactionHistoryViewDataList: [ReactionHistoryViewData] = []
// MARK: - Setup
class func instantiate(with viewModel: ReactionHistoryViewModelType) -> ReactionHistoryViewController {
let viewController = StoryboardScene.ReactionHistoryViewController.initialScene.instantiate()
viewController.viewModel = viewModel
viewController.theme = ThemeService.shared().theme
return viewController
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = VectorL10n.reactionHistoryTitle
self.viewModel.viewDelegate = self
self.activityPresenter = ActivityIndicatorPresenter()
self.errorPresenter = MXKErrorAlertPresentation()
self.setupViews()
self.registerThemeServiceDidChangeThemeNotification()
self.update(theme: self.theme)
self.viewModel.process(viewAction: .loadMore)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if self.isViewAppearedOnce == false {
self.isViewAppearedOnce = true
}
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return self.theme.statusBarStyle
}
// MARK: - Private
private func update(theme: Theme) {
self.theme = theme
self.view.backgroundColor = theme.headerBackgroundColor
self.tableView.backgroundColor = theme.backgroundColor
if let navigationBar = self.navigationController?.navigationBar {
theme.applyStyle(onNavigationBar: navigationBar)
}
}
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@objc private func themeDidChange() {
self.update(theme: ThemeService.shared().theme)
}
private func setupTableView() {
self.tableView.contentInset = TableView.contentInset
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.estimatedRowHeight = TableView.estimatedRowHeight
self.tableView.register(cellType: ReactionHistoryViewCell.self)
self.tableView.tableFooterView = UIView()
}
private func setupViews() {
let closeBarButtonItem = MXKBarButtonItem(title: VectorL10n.close, style: .plain) { [weak self] in
self?.closeButtonAction()
}
self.navigationItem.rightBarButtonItem = closeBarButtonItem
self.setupTableView()
}
private func render(viewState: ReactionHistoryViewState) {
switch viewState {
case .loading:
self.renderLoading()
case .loaded(reactionHistoryViewDataList: let reactionHistoryViewDataList, allDataLoaded: let allDataLoaded):
self.renderLoaded(reactionHistoryViewDataList: reactionHistoryViewDataList, allDataLoaded: allDataLoaded)
case .error(let error):
self.render(error: error)
}
}
private func renderLoading() {
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
}
private func renderLoaded(reactionHistoryViewDataList: [ReactionHistoryViewData], allDataLoaded: Bool) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.reactionHistoryViewDataList = reactionHistoryViewDataList
self.tableView.reloadData()
}
private func render(error: Error) {
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
}
// MARK: - Actions
private func closeButtonAction() {
self.viewModel.process(viewAction: .close)
}
}
// MARK: - UITableViewDataSource
extension ReactionHistoryViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.reactionHistoryViewDataList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let reactionHistoryCell = tableView.dequeueReusableCell(for: indexPath, cellType: ReactionHistoryViewCell.self)
let reactionHistoryViewData = self.reactionHistoryViewDataList[indexPath.row]
reactionHistoryCell.update(theme: self.theme)
reactionHistoryCell.fill(with: reactionHistoryViewData)
return reactionHistoryCell
}
}
// MARK: - UITableViewDelegate
extension ReactionHistoryViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard self.isViewAppearedOnce else {
return
}
// Check if a scroll beyond scroll view content occurs
let distanceFromBottom = scrollView.contentSize.height - scrollView.contentOffset.y
if distanceFromBottom < scrollView.frame.size.height {
self.viewModel.process(viewAction: .loadMore)
}
}
}
// MARK: - ReactionHistoryViewModelViewDelegate
extension ReactionHistoryViewController: ReactionHistoryViewModelViewDelegate {
func reactionHistoryViewModel(_ viewModel: ReactionHistoryViewModelType, didUpdateViewState viewSate: ReactionHistoryViewState) {
self.render(viewState: viewSate)
}
}

View file

@ -0,0 +1,23 @@
/*
Copyright 2019 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 ReactionHistoryViewData {
let reaction: String
let userDisplayName: String
let dateString: String
}

View file

@ -0,0 +1,179 @@
// File created from ScreenTemplate
// $ createScreen.sh ReactionHistory ReactionHistory
/*
Copyright 2019 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
final class ReactionHistoryViewModel: ReactionHistoryViewModelType {
// MARK: - Constants
private enum Pagination {
static let count: UInt = 30
}
// MARK: - Properties
// MARK: Private
private let session: MXSession
private let roomId: String
private let eventId: String
private let aggregations: MXAggregations
private let eventFormatter: MXKEventFormatter
private let reactionsFormattingQueue: DispatchQueue
private var reactionHistoryViewDataList: [ReactionHistoryViewData] = []
private var operation: MXHTTPOperation?
private var nextBatch: String?
private var viewState: ReactionHistoryViewState?
private lazy var roomMembers: MXRoomMembers? = {
return buildRoomMembers()
}()
// MARK: Public
weak var viewDelegate: ReactionHistoryViewModelViewDelegate?
weak var coordinatorDelegate: ReactionHistoryViewModelCoordinatorDelegate?
// MARK: - Setup
init(session: MXSession, roomId: String, eventId: String) {
self.session = session
self.aggregations = session.aggregations
self.roomId = roomId
self.eventId = eventId
self.eventFormatter = EventFormatter(matrixSession: session)
self.reactionsFormattingQueue = DispatchQueue(label: "\(type(of: self)).reactionsFormattingQueue")
}
// MARK: - Public
func process(viewAction: ReactionHistoryViewAction) {
switch viewAction {
case .loadMore:
self.loadMoreHistory()
case .close:
self.coordinatorDelegate?.reactionHistoryViewModelDidClose(self)
}
}
// MARK: - Private
private func canLoadMoreHistory() -> Bool {
guard let viewState = self.viewState else {
return true
}
let canLoadMoreHistory: Bool
switch viewState {
case .loading:
canLoadMoreHistory = false
case .loaded(reactionHistoryViewDataList: _, allDataLoaded: let allDataLoaded):
canLoadMoreHistory = !allDataLoaded
default:
canLoadMoreHistory = true
}
return canLoadMoreHistory
}
private func loadMoreHistory() {
guard self.canLoadMoreHistory() else {
print("[ReactionHistoryViewModel] loadMoreHistory: pending loading or all data loaded")
return
}
guard self.operation == nil else {
print("[ReactionHistoryViewModel] loadMoreHistory: operation already pending")
return
}
self.update(viewState: .loading)
self.operation = self.aggregations.reactionsEvents(forEvent: self.eventId, inRoom: self.roomId, from: self.nextBatch, limit: Pagination.count, success: { [weak self] (response) in
guard let self = self else {
return
}
self.nextBatch = response.nextBatch
self.operation = nil
self.process(reactionEvents: response.chunk, nextBatch: response.nextBatch)
}, failure: { [weak self] error in
guard let self = self else {
return
}
self.operation = nil
self.update(viewState: .error(error))
})
}
private func process(reactionEvents: [MXEvent], nextBatch: String?) {
self.reactionsFormattingQueue.async {
let reactionHistoryList = reactionEvents.compactMap { (reactionEvent) -> ReactionHistoryViewData? in
return self.reactionHistoryViewData(from: reactionEvent)
}
self.reactionHistoryViewDataList.append(contentsOf: reactionHistoryList)
let allDataLoaded = nextBatch == nil
DispatchQueue.main.async {
self.update(viewState: .loaded(reactionHistoryViewDataList: self.reactionHistoryViewDataList, allDataLoaded: allDataLoaded))
}
}
}
private func reactionHistoryViewData(from reactionEvent: MXEvent) -> ReactionHistoryViewData? {
guard let userId = reactionEvent.sender,
let reaction = reactionEvent.relatesTo?.key,
let reactionDateString = self.eventFormatter.dateString(fromTimestamp: reactionEvent.originServerTs, withTime: true) else {
return nil
}
let userDisplayName = self.userDisplayName(from: userId) ?? userId
return ReactionHistoryViewData(reaction: reaction, userDisplayName: userDisplayName, dateString: reactionDateString)
}
private func userDisplayName(from userId: String) -> String? {
guard let roomMembers = self.roomMembers else {
return nil
}
let roomMember = roomMembers.member(withUserId: userId)
return roomMember?.displayname
}
private func buildRoomMembers() -> MXRoomMembers? {
guard let room = self.session.room(withRoomId: self.roomId) else {
return nil
}
return room.dangerousSyncState?.members
}
private func update(viewState: ReactionHistoryViewState) {
self.viewState = viewState
self.viewDelegate?.reactionHistoryViewModel(self, didUpdateViewState: viewState)
}
}

View file

@ -0,0 +1,36 @@
// File created from ScreenTemplate
// $ createScreen.sh ReactionHistory ReactionHistory
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
protocol ReactionHistoryViewModelViewDelegate: class {
func reactionHistoryViewModel(_ viewModel: ReactionHistoryViewModelType, didUpdateViewState viewSate: ReactionHistoryViewState)
}
protocol ReactionHistoryViewModelCoordinatorDelegate: class {
func reactionHistoryViewModelDidClose(_ viewModel: ReactionHistoryViewModelType)
}
/// Protocol describing the view model used by `ReactionHistoryViewController`
protocol ReactionHistoryViewModelType {
var viewDelegate: ReactionHistoryViewModelViewDelegate? { get set }
var coordinatorDelegate: ReactionHistoryViewModelCoordinatorDelegate? { get set }
func process(viewAction: ReactionHistoryViewAction)
}

View file

@ -0,0 +1,26 @@
// File created from ScreenTemplate
// $ createScreen.sh ReactionHistory ReactionHistory
/*
Copyright 2019 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
/// ReactionHistoryViewController view state
enum ReactionHistoryViewState {
case loading
case loaded(reactionHistoryViewDataList: [ReactionHistoryViewData], allDataLoaded: Bool)
case error(Error)
}

View file

@ -124,7 +124,8 @@
#import "Riot-Swift.h"
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, RoomTitleViewTapGestureDelegate, RoomParticipantsViewControllerDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate>
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate>
{
// The expanded header
ExpandedRoomTitleView *expandedHeader;
@ -223,6 +224,10 @@
@property (nonatomic, strong) NSString *textMessageBeforeEditing;
@property (nonatomic, strong) EditHistoryCoordinatorBridgePresenter *editHistoryPresenter;
@property (nonatomic, strong) MXKDocumentPickerPresenter *documentPickerPresenter;
@property (nonatomic, strong) EmojiPickerCoordinatorBridgePresenter *emojiPickerCoordinatorBridgePresenter;
@property (nonatomic, strong) ReactionHistoryCoordinatorBridgePresenter *reactionHistoryCoordinatorBridgePresenter;
@property (nonatomic, strong) CameraPresenter *cameraPresenter;
@property (nonatomic, strong) MediaPickerCoordinatorBridgePresenter *mediaPickerPresenter;
@end
@ -1530,6 +1535,54 @@
}
}
- (void)showReactionHistoryForEventId:(NSString*)eventId animated:(BOOL)animated
{
if (self.reactionHistoryCoordinatorBridgePresenter.isPresenting)
{
return;
}
ReactionHistoryCoordinatorBridgePresenter *presenter = [[ReactionHistoryCoordinatorBridgePresenter alloc] initWithSession:self.mainSession roomId:self.roomDataSource.roomId eventId:eventId];
presenter.delegate = self;
[presenter presentFrom:self animated:animated];
self.reactionHistoryCoordinatorBridgePresenter = presenter;
}
- (void)showCameraControllerAnimated:(BOOL)animated
{
CameraPresenter *cameraPresenter = [CameraPresenter new];
cameraPresenter.delegate = self;
[cameraPresenter presentCameraFrom:self with:@[MXKUTI.image, MXKUTI.movie] animated:YES];
self.cameraPresenter = cameraPresenter;
}
- (void)showMediaPickerAnimated:(BOOL)animated
{
MediaPickerCoordinatorBridgePresenter *mediaPickerPresenter = [[MediaPickerCoordinatorBridgePresenter alloc] initWithSession:self.mainSession mediaUTIs:@[MXKUTI.image, MXKUTI.movie] allowsMultipleSelection:YES];
mediaPickerPresenter.delegate = self;
UIView *sourceView;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
sourceView = roomInputToolbarView.attachMediaButton;
}
else
{
sourceView = self.inputToolbarView;
}
[mediaPickerPresenter presentFrom:self sourceView:sourceView sourceRect:sourceView.bounds animated:YES];
self.mediaPickerPresenter = mediaPickerPresenter;
}
#pragma mark - Hide/Show expanded header
- (void)showExpandedHeader:(BOOL)isVisible
@ -2172,6 +2225,14 @@
[self handleLongPressFromCell:cell withTappedEvent:tappedEvent];
}
}
else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnReactionView])
{
NSString *tappedEventId = userInfo[kMXKRoomBubbleCellEventIdKey];
if (tappedEventId)
{
[self showReactionHistoryForEventId:tappedEventId animated:YES];
}
}
else
{
// Keep default implementation for other actions
@ -2532,6 +2593,20 @@
}]];
// Add reaction history if event contains reactions
if (roomBubbleTableViewCell.bubbleData.reactions[selectedEvent.eventId].aggregatedReactionsWithNonZeroCount)
{
[currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_reaction_history", @"Vector", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self cancelEventSelection];
// Show reaction history
[self showReactionHistoryForEventId:selectedEvent.eventId animated:YES];
}]];
}
[currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_view_source", @"Vector", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
@ -2710,19 +2785,9 @@
// Do not display empty action sheet
if (currentAlert.actions.count > 1)
{
NSArray *components = roomBubbleTableViewCell.bubbleData.bubbleComponents;
NSInteger bubbleComponentIndex = [roomBubbleTableViewCell.bubbleData bubbleComponentIndexForEventId:selectedEvent.eventId];
NSInteger index = 0;
for (MXKRoomBubbleComponent *component in components)
{
if ([component.event.eventId isEqualToString:selectedEvent.eventId])
{
break;
}
index++;
}
CGRect sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:index];
CGRect sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:bubbleComponentIndex];
[currentAlert mxk_setAccessibilityIdentifier:@"RoomVCEventMenuAlert"];
[currentAlert popoverPresentationController].sourceView = roomBubbleTableViewCell;
@ -3368,6 +3433,16 @@
self.documentPickerPresenter = documentPickerPresenter;
}
- (void)roomInputToolbarViewDidTapCamera:(MXKRoomInputToolbarView*)toolbarView
{
[self showCameraControllerAnimated:YES];
}
- (void)roomInputToolbarViewDidTapMediaLibrary:(MXKRoomInputToolbarView*)toolbarView
{
[self showMediaPickerAnimated:YES];
}
#pragma mark - RoomParticipantsViewControllerDelegate
- (void)roomParticipantsViewController:(RoomParticipantsViewController *)roomParticipantsViewController mention:(MXRoomMember*)member
@ -5200,15 +5275,7 @@
MXKRoomBubbleCellData *bubbleCellData = roomBubbleTableViewCell.bubbleData;
NSArray *bubbleComponents = bubbleCellData.bubbleComponents;
NSInteger foundComponentIndex = [bubbleComponents indexOfObjectPassingTest:^BOOL(MXKRoomBubbleComponent * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.event.eventId == selectedEventId)
{
*stop = YES;
return YES;
}
return NO;
}];
NSInteger foundComponentIndex = [bubbleCellData bubbleComponentIndexForEventId:event.eventId];
CGRect bubbleComponentFrame;
if (bubbleComponents.count > 0)
@ -5334,6 +5401,37 @@
}];
}
- (void)reactionsMenuViewModelDidTapMoreReactions:(ReactionsMenuViewModel *)viewModel forEventId:(NSString *)eventId
{
[self hideContextualMenuAnimated:YES];
EmojiPickerCoordinatorBridgePresenter *emojiPickerCoordinatorBridgePresenter = [[EmojiPickerCoordinatorBridgePresenter alloc] initWithSession:self.mainSession roomId:self.roomDataSource.roomId eventId:eventId];
emojiPickerCoordinatorBridgePresenter.delegate = self;
NSInteger cellRow = [self.roomDataSource indexOfCellDataWithEventId:eventId];
UIView *sourceView;
CGRect sourceRect = CGRectNull;
if (cellRow >= 0)
{
NSIndexPath *cellIndexPath = [NSIndexPath indexPathForRow:cellRow inSection:0];
UITableViewCell *cell = [self.bubblesTableView cellForRowAtIndexPath:cellIndexPath];
sourceView = cell;
if ([cell isKindOfClass:[MXKRoomBubbleTableViewCell class]])
{
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell;
NSInteger bubbleComponentIndex = [roomBubbleTableViewCell.bubbleData bubbleComponentIndexForEventId:eventId];
sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:bubbleComponentIndex];
}
}
[emojiPickerCoordinatorBridgePresenter presentFrom:self sourceView:sourceView sourceRect:sourceRect animated:YES];
self.emojiPickerCoordinatorBridgePresenter = emojiPickerCoordinatorBridgePresenter;
}
#pragma mark -
- (void)showEditHistoryForEventId:(NSString*)eventId animated:(BOOL)animated
@ -5401,5 +5499,131 @@
}
}
#pragma mark - EmojiPickerCoordinatorBridgePresenterDelegate
- (void)emojiPickerCoordinatorBridgePresenter:(EmojiPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didAddEmoji:(NSString *)emoji forEventId:(NSString *)eventId
{
MXWeakify(self);
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
[self.roomDataSource addReaction:emoji forEventId:eventId success:^{
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
[self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
}];
}];
self.emojiPickerCoordinatorBridgePresenter = nil;
}
- (void)emojiPickerCoordinatorBridgePresenter:(EmojiPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didRemoveEmoji:(NSString *)emoji forEventId:(NSString *)eventId
{
MXWeakify(self);
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
[self.roomDataSource removeReaction:emoji forEventId:eventId success:^{
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
[self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil];
}];
}];
self.emojiPickerCoordinatorBridgePresenter = nil;
}
- (void)emojiPickerCoordinatorBridgePresenterDidCancel:(EmojiPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
self.emojiPickerCoordinatorBridgePresenter = nil;
}
#pragma mark - ReactionHistoryCoordinatorBridgePresenterDelegate
- (void)reactionHistoryCoordinatorBridgePresenterDelegateDidClose:(ReactionHistoryCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:^{
self.reactionHistoryCoordinatorBridgePresenter = nil;
}];
}
#pragma mark - CameraPresenterDelegate
- (void)cameraPresenterDidCancel:(CameraPresenter *)cameraPresenter
{
[cameraPresenter dismissWithAnimated:YES completion:nil];
self.cameraPresenter = nil;
}
- (void)cameraPresenter:(CameraPresenter *)cameraPresenter didSelectImageData:(NSData *)imageData withUTI:(MXKUTI *)uti
{
[cameraPresenter dismissWithAnimated:YES completion:nil];
self.cameraPresenter = nil;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:MXKRoomInputToolbarCompressionModePrompt isPhotoLibraryAsset:NO];
}
}
- (void)cameraPresenter:(CameraPresenter *)cameraPresenter didSelectVideoAt:(NSURL *)url
{
[cameraPresenter dismissWithAnimated:YES completion:nil];
self.cameraPresenter = nil;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedVideo:url isPhotoLibraryAsset:NO];
}
}
#pragma mark - MediaPickerCoordinatorBridgePresenterDelegate
- (void)mediaPickerCoordinatorBridgePresenterDidCancel:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
self.mediaPickerPresenter = nil;
}
- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectImageData:(NSData *)imageData withUTI:(MXKUTI *)uti
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
self.mediaPickerPresenter = nil;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:MXKRoomInputToolbarCompressionModePrompt isPhotoLibraryAsset:YES];
}
}
- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectVideoAt:(NSURL *)url
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
self.mediaPickerPresenter = nil;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedVideo:url isPhotoLibraryAsset:YES];
}
}
- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectAssets:(NSArray<PHAsset *> *)assets
{
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
self.mediaPickerPresenter = nil;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedAssets:assets withCompressionMode:MXKRoomInputToolbarCompressionModePrompt];
}
}
@end

View file

@ -45,7 +45,7 @@ typedef enum : NSUInteger {
} RoomSettingsViewControllerField;
@interface RoomSettingsViewController : MXKRoomSettingsViewController <UITextViewDelegate, UITextFieldDelegate, MediaPickerViewControllerDelegate, MXKRoomMemberDetailsViewControllerDelegate, TableViewCellWithCheckBoxesDelegate>
@interface RoomSettingsViewController : MXKRoomSettingsViewController <UITextViewDelegate, UITextFieldDelegate, MXKRoomMemberDetailsViewControllerDelegate, TableViewCellWithCheckBoxesDelegate>
/**
Select a settings field in order to edit it ('RoomSettingsViewControllerFieldNone' by default).

Some files were not shown because too many files have changed in this diff Show more