diff --git a/CHANGES.rst b/CHANGES.rst index 6f580afbc5..c08d93e5ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,38 @@ +Changes in 0.9.0 (2019-07-16) +=============================================== + +Improvements: + * Upgrade MatrixKit version ([v0.10.1](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.10.1)). + * Upgrade MatrixKit version ([v0.10.0](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.10.0)). + * RoomVC: When replying, use a "Reply" button instead of "Send". + * RoomVC: New message actions (#2394). + * Room upgrade: Autojoin the upgraded room when the user taps on the tombstone banner (#2486). + * Room upgrade: Use the `server_name` parameter when joining the new room (#2550). + * Join Room: Support via parameters to better handle federation (#2547). + * Reactions: Display existing reactions below the message (#2396). + * Menu actions: Display message time (#2463). + * Reactions Menu: Fix position (#2447). + * Context menu polish (#2466). + * Upgrade Piwik/MatomoTracker (v6.0.1) (#2159). + * Message Editing: Annotate edited messages in timeline (#2400). + * Message Editing: Editing in the timeline (#2404). + * Read receipts: They are now counted at the MatrixKit level. + * Migrate to Swift 5.0. + * Reactions: Update quick reactions (#2459). + * Message Editing: Handle reply edition (#2492). + * RoomVC: Add ability to upload a file that comes from outside the app’s sandbox (#2019). + * Share extension: Enable any file upload (max 5). + * Tools: Create filterCryptoLogs.sh to filter logs related to e2ee from Riot logs. + +Bug fix: + * Device Verification: Fix user display name and device id colors in dark theme + * Device Verification: Name for 🔒 is "Lock" (#2526). + * Device Verification: Name for ⏰ is "Clock. + * Registration with an email is broken (#2417). + * Reactions: Bad position (#2462). + * Reactions: It lets you react to join/leave events (#2476). + * Adjust size of the insert button in the People tab, thanks to @dcordero (PR #2473). + Changes in 0.8.6 (2019-05-06) =============================================== @@ -35,7 +70,7 @@ Changes in 0.8.4 (2019-03-21) Improvements: * Upgrade MatrixKit version ([v0.9.8](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.9.8)). * Share extension: Remove image large size resizing choice if output dimension is too high to prevent memory limit exception (PR #2342). - + Bug fix: * Unable to open a file attachment of a room message (#2338). @@ -44,7 +79,7 @@ Changes in 0.8.3 (2019-03-13) Improvements: * Upgrade MatrixKit version ([v0.9.7](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.9.7)). - + Bug fix: * Widgets: Attempt to re-register for a scalar token if ours is invalid (#2326). * Widgets: Pass scalar_token only when required. @@ -106,7 +141,7 @@ Improvements: * Key backup: Update key backup setup UI and UX (PR #2243). * Key backup: Logout warning (#2245). * Key backup: new recover method detected (#2230). - + Bug fix: * Use white scroll bar on dark themes (#2158). * Registration: fix tap gesture on checkboxes in the terms screen. diff --git a/Gemfile b/Gemfile index 49ffe69fb4..1498a67105 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" gem "xcode-install" gem "fastlane" -gem "cocoapods", '~>1.6.0' +gem "cocoapods", '~>1.7.2' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 176b06f105..62c17a244a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (3.0.0) - activesupport (4.2.11) + activesupport (4.2.11.1) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) @@ -12,11 +12,11 @@ GEM atomos (0.1.3) babosa (1.0.2) claide (1.0.2) - cocoapods (1.6.1) + cocoapods (1.7.2) activesupport (>= 4.0.2, < 5) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.6.1) - cocoapods-deintegrate (>= 1.0.2, < 2.0) + cocoapods-core (= 1.7.2) + cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 1.2.2, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) @@ -25,17 +25,17 @@ GEM cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) - fourflusher (>= 2.2.0, < 3.0) + fourflusher (>= 2.3.0, < 3.0) gh_inspector (~> 1.0) molinillo (~> 0.6.6) nap (~> 1.0) ruby-macho (~> 1.4) - xcodeproj (>= 1.8.1, < 2.0) - cocoapods-core (1.6.1) + xcodeproj (>= 1.10.0, < 2.0) + cocoapods-core (1.7.2) activesupport (>= 4.0.2, < 6) fuzzy_match (~> 2.0.4) nap (~> 1.0) - cocoapods-deintegrate (1.0.3) + cocoapods-deintegrate (1.0.4) cocoapods-downloader (1.2.2) cocoapods-plugins (1.0.0) nap @@ -49,16 +49,16 @@ GEM colored2 (3.1.2) commander-fastlane (4.4.6) highline (~> 1.7.2) - concurrent-ruby (1.1.4) + concurrent-ruby (1.1.5) declarative (0.0.10) declarative-option (0.1.0) digest-crc (0.4.1) domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.1) + dotenv (2.7.2) emoji_regex (1.0.1) escape (0.0.4) - excon (0.62.0) + excon (0.64.0) faraday (0.15.4) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) @@ -67,7 +67,7 @@ GEM faraday_middleware (0.13.1) faraday (>= 0.7.4, < 1.0) fastimage (2.1.5) - fastlane (2.116.1) + fastlane (2.125.2) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) babosa (>= 1.0.2, < 2.0.0) @@ -86,8 +86,8 @@ GEM google-cloud-storage (>= 1.15.0, < 2.0.0) highline (>= 1.7.2, < 2.0.0) json (< 3.0.0) + jwt (~> 2.1.0) mini_magick (~> 4.5.1) - multi_json multi_xml (~> 0.5) multipart-post (~> 2.0.0) plist (>= 3.1.0, < 4.0.0) @@ -96,16 +96,16 @@ GEM security (= 0.1.3) simctl (~> 1.6.3) slack-notifier (>= 2.0.0, < 3.0.0) - terminal-notifier (>= 1.6.2, < 2.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (>= 1.4.5, < 2.0.0) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) - xcodeproj (>= 1.6.0, < 2.0.0) + xcodeproj (>= 1.8.1, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) fastlane-plugin-versioning (0.3.4) - fourflusher (2.2.0) + fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) google-api-client (0.23.9) @@ -118,7 +118,7 @@ GEM signet (~> 0.9) google-cloud-core (1.3.0) google-cloud-env (~> 1.0) - google-cloud-env (1.0.5) + google-cloud-env (1.1.0) faraday (~> 0.11) google-cloud-storage (1.16.0) digest-crc (~> 0.4) @@ -143,7 +143,7 @@ GEM memoist (0.16.0) mime-types (3.2.2) mime-types-data (~> 3.2015) - mime-types-data (3.2018.0812) + mime-types-data (3.2019.0331) mini_magick (4.5.1) minitest (5.11.3) molinillo (0.6.6) @@ -154,7 +154,7 @@ GEM nap (1.1.0) naturally (2.2.0) netrc (0.11.0) - os (1.0.0) + os (1.0.1) plist (3.5.0) public_suffix (2.0.5) representable (3.0.4) @@ -164,7 +164,7 @@ GEM retriable (3.1.2) rouge (2.0.7) ruby-macho (1.4.0) - rubyzip (1.2.2) + rubyzip (1.2.3) security (0.1.3) signet (0.11.0) addressable (~> 2.3) @@ -175,26 +175,26 @@ GEM CFPropertyList naturally slack-notifier (2.3.2) - terminal-notifier (1.8.0) + terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) thread_safe (0.3.6) - tty-cursor (0.6.0) - tty-screen (0.6.5) - tty-spinner (0.9.0) - tty-cursor (~> 0.6.0) + tty-cursor (0.7.0) + tty-screen (0.7.0) + tty-spinner (0.9.1) + tty-cursor (~> 0.7) tzinfo (1.2.5) thread_safe (~> 0.1) uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.5) - unicode-display_width (1.4.1) + unf_ext (0.0.7.6) + unicode-display_width (1.6.0) word_wrap (1.0.0) xcode-install (2.5.0) claide (>= 0.9.1, < 1.1.0) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.8.1) + xcodeproj (1.10.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -209,10 +209,10 @@ PLATFORMS ruby DEPENDENCIES - cocoapods (~> 1.6.0) + cocoapods (~> 1.7.2) fastlane fastlane-plugin-versioning xcode-install BUNDLED WITH - 1.17.3 + 2.0.2 diff --git a/Podfile b/Podfile index 280ff6c64c..c75eeeea71 100644 --- a/Podfile +++ b/Podfile @@ -7,7 +7,7 @@ use_frameworks! # Different flavours of pods to MatrixKit # The current MatrixKit pod version -$matrixKitVersion = '0.9.9' +$matrixKitVersion = '0.10.1' # The develop branch version #$matrixKitVersion = 'develop' @@ -60,25 +60,25 @@ end abstract_target 'RiotPods' do pod 'GBDeviceInfo', '~> 5.2.0' - pod 'Reusable', '~> 4.0' + pod 'Reusable', '~> 4.1' + pod 'SwiftUTI', :git => 'https://github.com/speramusinc/SwiftUTI.git', :branch => 'master' # Piwik for analytics - # While https://github.com/matomo-org/matomo-sdk-ios/pull/223 is not released, use the PR branch - pod 'PiwikTracker', :git => 'https://github.com/manuroe/matomo-sdk-ios.git', :branch => 'feature/CustomVariables' - #pod 'PiwikTracker', '~> 4.4.2' + pod 'MatomoTracker', '~> 6.0.1' # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true pod 'cmark', :inhibit_warnings => true pod 'DTCoreText', :inhibit_warnings => true pod 'zxcvbn-ios' - + # Tools pod 'SwiftGen', '~> 6.1' - pod 'SwiftLint', '~> 0.30.1' + pod 'SwiftLint', '~> 0.33.0' target "Riot" do import_MatrixKit + pod 'DGCollectionViewLeftAlignFlowLayout', '~> 1.0.4' end target "RiotShareExtension" do @@ -101,12 +101,11 @@ post_install do |installer| target.build_configurations.each do |config| config.build_settings['ENABLE_BITCODE'] = 'NO' - # Required for PiwikTracker as `swift_version` is not defined in podspec. Should be removed - if target.name.include? 'PiwikTracker' - config.build_settings['SWIFT_VERSION'] = '4.0' + # Force SwiftUTI Swift version to 5.0 (as there is no code changes to perform for SwiftUTI fork using Swift 4.2) + if target.name.include? 'SwiftUTI' + config.build_settings['SWIFT_VERSION'] = '5.0' end end - end end diff --git a/Podfile.lock b/Podfile.lock index 952f5eb90a..664d1aadf5 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -15,6 +15,7 @@ PODS: - AFNetworking/UIKit (3.2.1): - AFNetworking/NSURLSession - cmark (0.24.1) + - DGCollectionViewLeftAlignFlowLayout (1.0.4) - DTCoreText (1.6.21): - DTCoreText/Core (= 1.6.21) - DTFoundation/Core (~> 1.7.5) @@ -44,79 +45,86 @@ PODS: - HPGrowingTextView (1.1) - JitsiMeetSDK (2.1.0) - libbase58 (0.1.4) - - libPhoneNumber-iOS (0.9.13) - - MatrixKit (0.9.9): + - libPhoneNumber-iOS (0.9.15) + - MatomoTracker (6.0.1): + - MatomoTracker/Core (= 6.0.1) + - MatomoTracker/Core (6.0.1) + - MatrixKit (0.10.1): - cmark (~> 0.24.1) - DTCoreText (~> 1.6.21) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.9.9) - - MatrixSDK (= 0.12.5) - - MatrixKit/AppExtension (0.9.9): + - MatrixKit/Core (= 0.10.1) + - MatrixSDK (= 0.13.0) + - SwiftUTI (~> 1.0.6) + - MatrixKit/AppExtension (0.10.1): - cmark (~> 0.24.1) - DTCoreText (~> 1.6.21) - DTCoreText/Extension - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.12.5) - - MatrixKit/Core (0.9.9): + - MatrixSDK (= 0.13.0) + - SwiftUTI (~> 1.0.6) + - MatrixKit/Core (0.10.1): - cmark (~> 0.24.1) - DTCoreText (~> 1.6.21) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.12.5) - - MatrixSDK (0.12.5): - - MatrixSDK/Core (= 0.12.5) - - MatrixSDK/Core (0.12.5): + - MatrixSDK (= 0.13.0) + - SwiftUTI (~> 1.0.6) + - MatrixSDK (0.13.0): + - MatrixSDK/Core (= 0.13.0) + - MatrixSDK/Core (0.13.0): - AFNetworking (~> 3.2.0) - GZIP (~> 1.2.2) - libbase58 (~> 0.1.4) - OLMKit (~> 3.1.0) - Realm (~> 3.13.1) - - MatrixSDK/JingleCallStack (0.12.5): + - MatrixSDK/JingleCallStack (0.13.0): - JitsiMeetSDK (~> 2.1.0) - MatrixSDK/Core - - MatrixSDK/SwiftSupport (0.12.5): + - MatrixSDK/SwiftSupport (0.13.0): - MatrixSDK/Core - OLMKit (3.1.0): - OLMKit/olmc (= 3.1.0) - OLMKit/olmcpp (= 3.1.0) - OLMKit/olmc (3.1.0) - OLMKit/olmcpp (3.1.0) - - PiwikTracker (4.4.2): - - PiwikTracker/Core (= 4.4.2) - - PiwikTracker/Core (4.4.2) - Realm (3.13.1): - Realm/Headers (= 3.13.1) - Realm/Headers (3.13.1) - - Reusable (4.0.5): - - Reusable/Storyboard (= 4.0.5) - - Reusable/View (= 4.0.5) - - Reusable/Storyboard (4.0.5) - - Reusable/View (4.0.5) + - Reusable (4.1.0): + - Reusable/Storyboard (= 4.1.0) + - Reusable/View (= 4.1.0) + - Reusable/Storyboard (4.1.0) + - Reusable/View (4.1.0) - SwiftGen (6.1.0) - - SwiftLint (0.30.1) + - SwiftLint (0.33.0) + - SwiftUTI (1.0.7) - zxcvbn-ios (1.0.4) DEPENDENCIES: - cmark + - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - DTCoreText - GBDeviceInfo (~> 5.2.0) - - MatrixKit (= 0.9.9) - - MatrixKit/AppExtension (= 0.9.9) + - MatomoTracker (~> 6.0.1) + - MatrixKit (= 0.10.1) + - MatrixKit/AppExtension (= 0.10.1) - MatrixSDK/JingleCallStack - MatrixSDK/SwiftSupport - OLMKit - - PiwikTracker (from `https://github.com/manuroe/matomo-sdk-ios.git`, branch `feature/CustomVariables`) - - Reusable (~> 4.0) + - Reusable (~> 4.1) - SwiftGen (~> 6.1) - - SwiftLint (~> 0.30.1) + - SwiftLint (~> 0.33.0) + - SwiftUTI (from `https://github.com/speramusinc/SwiftUTI.git`, branch `master`) - zxcvbn-ios SPEC REPOS: https://github.com/cocoapods/specs.git: - AFNetworking - cmark + - DGCollectionViewLeftAlignFlowLayout - DTCoreText - DTFoundation - GBDeviceInfo @@ -125,6 +133,7 @@ SPEC REPOS: - JitsiMeetSDK - libbase58 - libPhoneNumber-iOS + - MatomoTracker - MatrixKit - MatrixSDK - OLMKit @@ -135,18 +144,19 @@ SPEC REPOS: - zxcvbn-ios EXTERNAL SOURCES: - PiwikTracker: - :branch: feature/CustomVariables - :git: https://github.com/manuroe/matomo-sdk-ios.git + SwiftUTI: + :branch: master + :git: https://github.com/speramusinc/SwiftUTI.git CHECKOUT OPTIONS: - PiwikTracker: - :commit: dfb048f25f4eefbe13ff7515c3c1c2cad5d94491 - :git: https://github.com/manuroe/matomo-sdk-ios.git + SwiftUTI: + :commit: c21237f13e9fb31a07f3fcd5243c5cf79d75901c + :git: https://github.com/speramusinc/SwiftUTI.git SPEC CHECKSUMS: AFNetworking: b6f891fdfaed196b46c7a83cf209e09697b94057 - cmark: ec0275215b504780287b6fca360224e384368af8 + cmark: 1d9ad0375e3b9fa281732e992467903606015520 + DGCollectionViewLeftAlignFlowLayout: a0fa58797373ded039cafba8133e79373d048399 DTCoreText: e5d688cffc9f6a61eddd1a4f94e2046851230de3 DTFoundation: f03be9fd786f11e505bb8fc44e2a3732bf0917df GBDeviceInfo: 2c65ceb9404f9079264d4c238f5b81916fdfc5e2 @@ -154,17 +164,18 @@ SPEC CHECKSUMS: HPGrowingTextView: 88a716d97fb853bcb08a4a08e4727da17efc9b19 JitsiMeetSDK: 3e66564af7f38a19142338955dd7f581801852b3 libbase58: 7c040313537b8c44b6e2d15586af8e21f7354efd - libPhoneNumber-iOS: e444379ac18bbfbdefad571da735b2cd7e096caa - MatrixKit: 6f553797e1ad42794b5336afb5cecb975ec69daa - MatrixSDK: ed0d0cee4877955052f19730bb3ee727e01ec948 + libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 + MatomoTracker: 3ae4f65a1f5ace8043bda7244888fee28a734de5 + MatrixKit: f8224de32ca8b6e4c54a2654369cedec7744dc6d + MatrixSDK: 6886e7234c650408db5876b44a7f7608c865ce30 OLMKit: 4ee0159d63feeb86d836fdcfefe418e163511639 - PiwikTracker: 42862c7b13028065c3dfd36b4dc38db8a5765acf Realm: 50071da38fe079e0735e47c9f2eae738c68c5996 - Reusable: 188be1a54ac0691bc66e5bb24ec6eb91971b315b + Reusable: 82be188f29d96dc5eff0db7b2393bcc08d2cdd5b SwiftGen: f872ca75cbd17bf7103c17f13dcfa0d9a15667b0 - SwiftLint: a54bf1fe12b55c68560eb2a7689dfc81458508f7 + SwiftLint: fed9c66336e41fc74dc48a73678380718f0c8b0e + SwiftUTI: 917993c124f8eac25e88ced0202fc58d7eb50fa8 zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c -PODFILE CHECKSUM: cfb6be050dfbb227d58b14434629e447ea54554b +PODFILE CHECKSUM: 6b3ff49b9c446763a5629e71bdde3fe8da7ba93f -COCOAPODS: 1.6.1 +COCOAPODS: 1.7.2 diff --git a/README.rst b/README.rst index d1c56e902e..7e398492bc 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,8 @@ Before opening the Riot Xcode workspace, you need to build it with the CocoaPods command:: $ cd Riot - $ pod install + $ bundle install + $ bundle exec pod install This will load all dependencies for the Riot source code, including MatrixKit and MatrixSDK. You will need an recent and updated (``pod setup``) install of diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 08f2bccf2e..13306ca789 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -22,6 +22,7 @@ 32242F1721E8FBE500725742 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32242F0D21E8FBA900725742 /* Theme.swift */; }; 32242F1821E8FBF800725742 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32242F0F21E8FBA900725742 /* DefaultTheme.swift */; }; 32242F1921E8FBFB00725742 /* DarkTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32242F1021E8FBA900725742 /* DarkTheme.swift */; }; + 322C110822BBC6F80043FEAC /* WidgetManagerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 322C110722BBC6F80043FEAC /* WidgetManagerConfig.swift */; }; 3232AB1422564D9100AD6A5C /* swiftgen-config.yml in Resources */ = {isa = PBXBuildFile; fileRef = 3232AB0022564D9100AD6A5C /* swiftgen-config.yml */; }; 3232AB1522564D9100AD6A5C /* flat-swift4-vector.stencil in Resources */ = {isa = PBXBuildFile; fileRef = 3232AB0322564D9100AD6A5C /* flat-swift4-vector.stencil */; }; 3232AB2122564D9100AD6A5C /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 3232AB1322564D9100AD6A5C /* README.md */; }; @@ -74,7 +75,21 @@ 32891D712264DF7B00C82226 /* DeviceVerificationVerifiedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32891D6F2264DF7B00C82226 /* DeviceVerificationVerifiedViewController.swift */; }; 32891D75226728EE00C82226 /* DeviceVerificationDataLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32891D73226728EE00C82226 /* DeviceVerificationDataLoadingViewController.swift */; }; 32891D76226728EF00C82226 /* DeviceVerificationDataLoadingViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32891D74226728EE00C82226 /* DeviceVerificationDataLoadingViewController.storyboard */; }; + 329E746622CD02EA006F9797 /* BubbleReactionActionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 329E746422CD02EA006F9797 /* BubbleReactionActionViewCell.xib */; }; + 329E746722CD02EA006F9797 /* BubbleReactionActionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 329E746522CD02EA006F9797 /* BubbleReactionActionViewCell.swift */; }; + 32A6001622C661100042C1D9 /* EditHistoryViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6000D22C661100042C1D9 /* EditHistoryViewState.swift */; }; + 32A6001722C661100042C1D9 /* EditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6000E22C661100042C1D9 /* EditHistoryViewController.swift */; }; + 32A6001822C661100042C1D9 /* EditHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6000F22C661100042C1D9 /* EditHistoryViewModel.swift */; }; + 32A6001922C661100042C1D9 /* EditHistoryViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6001022C661100042C1D9 /* EditHistoryViewModelType.swift */; }; + 32A6001A22C661100042C1D9 /* EditHistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6001122C661100042C1D9 /* EditHistoryCoordinator.swift */; }; + 32A6001B22C661100042C1D9 /* EditHistoryViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6001222C661100042C1D9 /* EditHistoryViewAction.swift */; }; + 32A6001C22C661100042C1D9 /* EditHistoryViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32A6001322C661100042C1D9 /* EditHistoryViewController.storyboard */; }; + 32A6001D22C661100042C1D9 /* EditHistoryCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6001422C661100042C1D9 /* EditHistoryCoordinatorType.swift */; }; + 32A6001E22C661100042C1D9 /* EditHistoryCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6001522C661100042C1D9 /* EditHistoryCoordinatorBridgePresenter.swift */; }; + 32A6002022C66FCF0042C1D9 /* EditHistoryMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A6001F22C66FCF0042C1D9 /* EditHistoryMessage.swift */; }; 32B1FEDB21A46F2C00637127 /* TermsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32B1FEDA21A46F2C00637127 /* TermsView.xib */; }; + 32B94DF9228EC26400716A26 /* ReactionsMenuViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B94DF2228EC26400716A26 /* ReactionsMenuViewAction.swift */; }; + 32B94DFA228EC26400716A26 /* ReactionsMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B94DF3228EC26400716A26 /* ReactionsMenuButton.swift */; }; 32BF994F21FA29A400698084 /* SettingsKeyBackupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF994E21FA29A400698084 /* SettingsKeyBackupViewModel.swift */; }; 32BF995121FA29DC00698084 /* SettingsKeyBackupViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF995021FA29DC00698084 /* SettingsKeyBackupViewModelType.swift */; }; 32BF995321FA2A1300698084 /* SettingsKeyBackupViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BF995221FA2A1300698084 /* SettingsKeyBackupViewState.swift */; }; @@ -138,6 +153,8 @@ B140B4A221F87F7100E3F5FE /* OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = B140B4A121F87F7100E3F5FE /* OperationQueue.swift */; }; B140B4A621F89E7600E3F5FE /* KeyBackupSetupCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B140B4A521F89E7600E3F5FE /* KeyBackupSetupCoordinatorBridgePresenter.swift */; }; B140B4A821F8AB4600E3F5FE /* KeyBackupRecoverCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B140B4A721F8AB4600E3F5FE /* KeyBackupRecoverCoordinatorBridgePresenter.swift */; }; + B142317A22CCFA2000FFA96A /* EditHistoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B142317822CCFA2000FFA96A /* EditHistoryCell.swift */; }; + B142317B22CCFA2000FFA96A /* EditHistoryCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B142317922CCFA2000FFA96A /* EditHistoryCell.xib */; }; B14F142E22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B14F142622144F6400FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.storyboard */; }; B14F142F22144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14F142722144F6400FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModelType.swift */; }; B14F143022144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14F142822144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyCoordinatorType.swift */; }; @@ -187,8 +204,22 @@ B169331720F3CBE000746532 /* RecentCellData.m in Sources */ = {isa = PBXBuildFile; fileRef = B16932F920F3C51900746532 /* RecentCellData.m */; }; B17982FF2119FED2001FD722 /* GDPRConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17982FE2119FED2001FD722 /* GDPRConsentViewController.swift */; }; B1798302211B13B3001FD722 /* OnBoardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1798301211B13B3001FD722 /* OnBoardingManager.swift */; }; + B190F55922CE356800AEB493 /* EditHistoryHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B190F55822CE356800AEB493 /* EditHistoryHeaderView.swift */; }; + B190F55B22CE35FD00AEB493 /* EditHistoryHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B190F55A22CE35FD00AEB493 /* EditHistoryHeaderView.xib */; }; + B190F55D22CE5A9700AEB493 /* EditHistorySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B190F55C22CE5A9600AEB493 /* EditHistorySection.swift */; }; + B1963B2B228F1C4900CBA17F /* BubbleReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B25228F1C4800CBA17F /* BubbleReactionsView.swift */; }; + B1963B2C228F1C4900CBA17F /* BubbleReactionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1963B26228F1C4800CBA17F /* BubbleReactionViewCell.xib */; }; + B1963B2D228F1C4900CBA17F /* BubbleReactionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B27228F1C4800CBA17F /* BubbleReactionsViewModel.swift */; }; + B1963B2E228F1C4900CBA17F /* BubbleReactionViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B28228F1C4800CBA17F /* BubbleReactionViewData.swift */; }; + B1963B2F228F1C4900CBA17F /* BubbleReactionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B29228F1C4800CBA17F /* BubbleReactionViewCell.swift */; }; + B1963B30228F1C4900CBA17F /* BubbleReactionsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1963B2A228F1C4800CBA17F /* BubbleReactionsView.xib */; }; + B1963B32228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B31228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift */; }; + B1963B3822933BC800CBA17F /* AutosizedCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1963B3722933BC800CBA17F /* AutosizedCollectionView.swift */; }; B19EFA3921F8BB2C00FC070E /* KeyBackupRecoverCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19EFA3821F8BB2C00FC070E /* KeyBackupRecoverCoordinatorType.swift */; }; B19EFA3B21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19EFA3A21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift */; }; + B1A5B33E227ADF2A004CBA85 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A5B33D227ADF2A004CBA85 /* UIImage.swift */; }; + B1A68593229E807A00D6C09A /* RoomBubbleCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A68592229E807800D6C09A /* RoomBubbleCellLayout.swift */; }; + B1B12B2922942315002CB419 /* UITouch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B12B2822942315002CB419 /* UITouch.swift */; }; B1B5571820EE6C4D00210D55 /* CountryPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5567A20EE6C4C00210D55 /* CountryPickerViewController.m */; }; B1B5571920EE6C4D00210D55 /* LanguagePickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5567C20EE6C4C00210D55 /* LanguagePickerViewController.m */; }; B1B5571A20EE6C4D00210D55 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = B1B5567E20EE6C4C00210D55 /* SettingsViewController.m */; }; @@ -426,10 +457,27 @@ 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 */; }; + 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 */; }; + B1C562DB228C0BB00037F12A /* RoomContextualMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562DA228C0BB00037F12A /* RoomContextualMenuAction.swift */; }; + B1C562E1228C7C8C0037F12A /* RoomContextualMenuToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562DC228C7C890037F12A /* RoomContextualMenuToolbarView.swift */; }; + B1C562E2228C7C8D0037F12A /* RoomContextualMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562DD228C7C8A0037F12A /* RoomContextualMenuViewController.swift */; }; + B1C562E3228C7C8D0037F12A /* RoomContextualMenuPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562DE228C7C8B0037F12A /* RoomContextualMenuPresenter.swift */; }; + B1C562E4228C7C8D0037F12A /* RoomContextualMenuToolbarView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1C562DF228C7C8C0037F12A /* RoomContextualMenuToolbarView.xib */; }; + B1C562E5228C7C8D0037F12A /* RoomContextualMenuViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B1C562E0228C7C8C0037F12A /* RoomContextualMenuViewController.storyboard */; }; + B1C562E8228C7CF20037F12A /* ContextualMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C562E6228C7CF10037F12A /* ContextualMenuItemView.swift */; }; + B1C562E9228C7CF20037F12A /* ContextualMenuItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1C562E7228C7CF20037F12A /* ContextualMenuItemView.xib */; }; B1CA3A2721EF6914000D1D89 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CA3A2621EF6913000D1D89 /* UIViewController.swift */; }; B1CA3A2921EF692B000D1D89 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CA3A2821EF692B000D1D89 /* UIView.swift */; }; B1CE9EFD22148703000FAE6A /* SignOutAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CE9EFC22148703000FAE6A /* SignOutAlertPresenter.swift */; }; B1CE9F062216FB09000FAE6A /* EncryptionKeysExportPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1CE9F052216FB09000FAE6A /* EncryptionKeysExportPresenter.swift */; }; + B1D1BDA622BBAFB500831367 /* ReactionsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D1BDA522BBAFB500831367 /* ReactionsMenuView.swift */; }; + B1D1BDA822BBAFC900831367 /* ReactionsMenuView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1D1BDA722BBAFC900831367 /* ReactionsMenuView.xib */; }; + B1D211E222BD193C00D939BD /* ReactionsMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D211E122BD193C00D939BD /* ReactionsMenuViewModel.swift */; }; + B1D211E422C18E3800D939BD /* ReactionsMenuViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D211E322C18E3800D939BD /* ReactionsMenuViewModelType.swift */; }; + B1D211E622C194A200D939BD /* ReactionsMenuViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D211E522C194A200D939BD /* ReactionsMenuViewState.swift */; }; + B1D211E822C195B400D939BD /* ReactionMenuItemViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D211E722C195B400D939BD /* ReactionMenuItemViewData.swift */; }; B1D250D82118AA0A000F4E93 /* RoomPredecessorBubbleCell.m in Sources */ = {isa = PBXBuildFile; fileRef = B1D250D72118AA0A000F4E93 /* RoomPredecessorBubbleCell.m */; }; B1D4752721EE4E630067973F /* KeyboardAvoider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D4752521EE4E620067973F /* KeyboardAvoider.swift */; }; B1D4752821EE4E630067973F /* KeyboardNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D4752621EE4E620067973F /* KeyboardNotification.swift */; }; @@ -519,6 +567,7 @@ 32242F0F21E8FBA900725742 /* DefaultTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = ""; }; 32242F1021E8FBA900725742 /* DarkTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkTheme.swift; sourceTree = ""; }; 32242F1121E8FBA900725742 /* ThemeService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThemeService.h; sourceTree = ""; }; + 322C110722BBC6F80043FEAC /* WidgetManagerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetManagerConfig.swift; sourceTree = ""; }; 3232AB0022564D9100AD6A5C /* swiftgen-config.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "swiftgen-config.yml"; sourceTree = ""; }; 3232AB0322564D9100AD6A5C /* flat-swift4-vector.stencil */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "flat-swift4-vector.stencil"; sourceTree = ""; }; 3232AB1322564D9100AD6A5C /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -575,7 +624,21 @@ 32891D6F2264DF7B00C82226 /* DeviceVerificationVerifiedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationVerifiedViewController.swift; sourceTree = ""; }; 32891D73226728EE00C82226 /* DeviceVerificationDataLoadingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceVerificationDataLoadingViewController.swift; sourceTree = ""; }; 32891D74226728EE00C82226 /* DeviceVerificationDataLoadingViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = DeviceVerificationDataLoadingViewController.storyboard; sourceTree = ""; }; + 329E746422CD02EA006F9797 /* BubbleReactionActionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BubbleReactionActionViewCell.xib; sourceTree = ""; }; + 329E746522CD02EA006F9797 /* BubbleReactionActionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleReactionActionViewCell.swift; sourceTree = ""; }; + 32A6000D22C661100042C1D9 /* EditHistoryViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditHistoryViewState.swift; sourceTree = ""; }; + 32A6000E22C661100042C1D9 /* EditHistoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditHistoryViewController.swift; sourceTree = ""; }; + 32A6000F22C661100042C1D9 /* EditHistoryViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditHistoryViewModel.swift; sourceTree = ""; }; + 32A6001022C661100042C1D9 /* EditHistoryViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditHistoryViewModelType.swift; sourceTree = ""; }; + 32A6001122C661100042C1D9 /* EditHistoryCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditHistoryCoordinator.swift; sourceTree = ""; }; + 32A6001222C661100042C1D9 /* EditHistoryViewAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditHistoryViewAction.swift; sourceTree = ""; }; + 32A6001322C661100042C1D9 /* EditHistoryViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = EditHistoryViewController.storyboard; sourceTree = ""; }; + 32A6001422C661100042C1D9 /* EditHistoryCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditHistoryCoordinatorType.swift; sourceTree = ""; }; + 32A6001522C661100042C1D9 /* EditHistoryCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditHistoryCoordinatorBridgePresenter.swift; sourceTree = ""; }; + 32A6001F22C66FCF0042C1D9 /* EditHistoryMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHistoryMessage.swift; sourceTree = ""; }; 32B1FEDA21A46F2C00637127 /* TermsView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TermsView.xib; sourceTree = ""; }; + 32B94DF2228EC26400716A26 /* ReactionsMenuViewAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionsMenuViewAction.swift; sourceTree = ""; }; + 32B94DF3228EC26400716A26 /* ReactionsMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionsMenuButton.swift; sourceTree = ""; }; 32BDC9A1211C2C870064AF51 /* zh_Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_Hant; path = zh_Hant.lproj/InfoPlist.strings; sourceTree = ""; }; 32BDC9A2211C2C870064AF51 /* zh_Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_Hant; path = zh_Hant.lproj/Localizable.strings; sourceTree = ""; }; 32BDC9A3211C2C870064AF51 /* zh_Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_Hant; path = zh_Hant.lproj/Vector.strings; sourceTree = ""; }; @@ -661,6 +724,8 @@ B140B4A121F87F7100E3F5FE /* OperationQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationQueue.swift; sourceTree = ""; }; B140B4A521F89E7600E3F5FE /* KeyBackupSetupCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyBackupSetupCoordinatorBridgePresenter.swift; sourceTree = ""; }; B140B4A721F8AB4600E3F5FE /* KeyBackupRecoverCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverCoordinatorBridgePresenter.swift; sourceTree = ""; }; + B142317822CCFA2000FFA96A /* EditHistoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHistoryCell.swift; sourceTree = ""; }; + B142317922CCFA2000FFA96A /* EditHistoryCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EditHistoryCell.xib; sourceTree = ""; }; B14F142622144F6400FA0595 /* KeyBackupRecoverFromRecoveryKeyViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = KeyBackupRecoverFromRecoveryKeyViewController.storyboard; sourceTree = ""; }; B14F142722144F6400FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromRecoveryKeyViewModelType.swift; sourceTree = ""; }; B14F142822144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverFromRecoveryKeyCoordinatorType.swift; sourceTree = ""; }; @@ -758,8 +823,22 @@ B169331320F3CAFC00746532 /* PublicRoomsDirectoryDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PublicRoomsDirectoryDataSource.h; sourceTree = ""; }; B17982FE2119FED2001FD722 /* GDPRConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GDPRConsentViewController.swift; sourceTree = ""; }; B1798301211B13B3001FD722 /* OnBoardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingManager.swift; sourceTree = ""; }; + B190F55822CE356800AEB493 /* EditHistoryHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHistoryHeaderView.swift; sourceTree = ""; }; + B190F55A22CE35FD00AEB493 /* EditHistoryHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EditHistoryHeaderView.xib; sourceTree = ""; }; + B190F55C22CE5A9600AEB493 /* EditHistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHistorySection.swift; sourceTree = ""; }; + B1963B25228F1C4800CBA17F /* BubbleReactionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleReactionsView.swift; sourceTree = ""; }; + B1963B26228F1C4800CBA17F /* BubbleReactionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BubbleReactionViewCell.xib; sourceTree = ""; }; + B1963B27228F1C4800CBA17F /* BubbleReactionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleReactionsViewModel.swift; sourceTree = ""; }; + B1963B28228F1C4800CBA17F /* BubbleReactionViewData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleReactionViewData.swift; sourceTree = ""; }; + B1963B29228F1C4800CBA17F /* BubbleReactionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleReactionViewCell.swift; sourceTree = ""; }; + B1963B2A228F1C4800CBA17F /* BubbleReactionsView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BubbleReactionsView.xib; sourceTree = ""; }; + B1963B31228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleReactionsViewModelType.swift; sourceTree = ""; }; + B1963B3722933BC800CBA17F /* AutosizedCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutosizedCollectionView.swift; sourceTree = ""; }; B19EFA3821F8BB2C00FC070E /* KeyBackupRecoverCoordinatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverCoordinatorType.swift; sourceTree = ""; }; B19EFA3A21F8BB4100FC070E /* KeyBackupRecoverCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupRecoverCoordinator.swift; sourceTree = ""; }; + B1A5B33D227ADF2A004CBA85 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; + B1A68592229E807800D6C09A /* RoomBubbleCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomBubbleCellLayout.swift; sourceTree = ""; }; + B1B12B2822942315002CB419 /* UITouch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITouch.swift; sourceTree = ""; }; B1B5567920EE6C4C00210D55 /* CountryPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CountryPickerViewController.h; sourceTree = ""; }; B1B5567A20EE6C4C00210D55 /* CountryPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CountryPickerViewController.m; sourceTree = ""; }; B1B5567C20EE6C4C00210D55 /* LanguagePickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LanguagePickerViewController.m; sourceTree = ""; }; @@ -1133,10 +1212,27 @@ B1B5599020EFC5E400210D55 /* Analytics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Analytics.h; sourceTree = ""; }; B1B5599120EFC5E400210D55 /* DecryptionFailureTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DecryptionFailureTracker.m; sourceTree = ""; }; B1B9194A2118984300FE25B5 /* RoomPredecessorBubbleCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RoomPredecessorBubbleCell.xib; sourceTree = ""; }; + B1C562C92289C2690037F12A /* UIGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIGestureRecognizer.swift; sourceTree = ""; }; + B1C562CB228AB3510037F12A /* UIStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackView.swift; sourceTree = ""; }; + B1C562D8228C0B760037F12A /* RoomContextualMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuItem.swift; sourceTree = ""; }; + B1C562DA228C0BB00037F12A /* RoomContextualMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuAction.swift; sourceTree = ""; }; + B1C562DC228C7C890037F12A /* RoomContextualMenuToolbarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuToolbarView.swift; sourceTree = ""; }; + B1C562DD228C7C8A0037F12A /* RoomContextualMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuViewController.swift; sourceTree = ""; }; + B1C562DE228C7C8B0037F12A /* RoomContextualMenuPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomContextualMenuPresenter.swift; sourceTree = ""; }; + B1C562DF228C7C8C0037F12A /* RoomContextualMenuToolbarView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomContextualMenuToolbarView.xib; sourceTree = ""; }; + B1C562E0228C7C8C0037F12A /* RoomContextualMenuViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = RoomContextualMenuViewController.storyboard; sourceTree = ""; }; + B1C562E6228C7CF10037F12A /* ContextualMenuItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextualMenuItemView.swift; sourceTree = ""; }; + B1C562E7228C7CF20037F12A /* ContextualMenuItemView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContextualMenuItemView.xib; sourceTree = ""; }; B1CA3A2621EF6913000D1D89 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; B1CA3A2821EF692B000D1D89 /* UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; B1CE9EFC22148703000FAE6A /* SignOutAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignOutAlertPresenter.swift; sourceTree = ""; }; B1CE9F052216FB09000FAE6A /* EncryptionKeysExportPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionKeysExportPresenter.swift; sourceTree = ""; }; + B1D1BDA522BBAFB500831367 /* ReactionsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsMenuView.swift; sourceTree = ""; }; + B1D1BDA722BBAFC900831367 /* ReactionsMenuView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReactionsMenuView.xib; sourceTree = ""; }; + B1D211E122BD193C00D939BD /* ReactionsMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsMenuViewModel.swift; sourceTree = ""; }; + B1D211E322C18E3800D939BD /* ReactionsMenuViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsMenuViewModelType.swift; sourceTree = ""; }; + B1D211E522C194A200D939BD /* ReactionsMenuViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsMenuViewState.swift; sourceTree = ""; }; + B1D211E722C195B400D939BD /* ReactionMenuItemViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionMenuItemViewData.swift; sourceTree = ""; }; B1D250D62118AA0A000F4E93 /* RoomPredecessorBubbleCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RoomPredecessorBubbleCell.h; sourceTree = ""; }; B1D250D72118AA0A000F4E93 /* RoomPredecessorBubbleCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RoomPredecessorBubbleCell.m; sourceTree = ""; }; B1D4752521EE4E620067973F /* KeyboardAvoider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardAvoider.swift; sourceTree = ""; }; @@ -1447,6 +1543,43 @@ path = js; sourceTree = ""; }; + 32A6000C22C661100042C1D9 /* EditHistory */ = { + isa = PBXGroup; + children = ( + 32A6000D22C661100042C1D9 /* EditHistoryViewState.swift */, + 32A6000E22C661100042C1D9 /* EditHistoryViewController.swift */, + 32A6001322C661100042C1D9 /* EditHistoryViewController.storyboard */, + B190F55822CE356800AEB493 /* EditHistoryHeaderView.swift */, + B190F55A22CE35FD00AEB493 /* EditHistoryHeaderView.xib */, + B142317822CCFA2000FFA96A /* EditHistoryCell.swift */, + B142317922CCFA2000FFA96A /* EditHistoryCell.xib */, + 32A6000F22C661100042C1D9 /* EditHistoryViewModel.swift */, + 32A6001022C661100042C1D9 /* EditHistoryViewModelType.swift */, + 32A6001122C661100042C1D9 /* EditHistoryCoordinator.swift */, + 32A6001222C661100042C1D9 /* EditHistoryViewAction.swift */, + 32A6001422C661100042C1D9 /* EditHistoryCoordinatorType.swift */, + 32A6001522C661100042C1D9 /* EditHistoryCoordinatorBridgePresenter.swift */, + B190F55C22CE5A9600AEB493 /* EditHistorySection.swift */, + 32A6001F22C66FCF0042C1D9 /* EditHistoryMessage.swift */, + ); + path = EditHistory; + sourceTree = ""; + }; + 32B94DF0228EC26400716A26 /* ReactionsMenu */ = { + isa = PBXGroup; + children = ( + B1D211E322C18E3800D939BD /* ReactionsMenuViewModelType.swift */, + B1D211E122BD193C00D939BD /* ReactionsMenuViewModel.swift */, + B1D211E722C195B400D939BD /* ReactionMenuItemViewData.swift */, + B1D1BDA522BBAFB500831367 /* ReactionsMenuView.swift */, + B1D1BDA722BBAFC900831367 /* ReactionsMenuView.xib */, + B1D211E522C194A200D939BD /* ReactionsMenuViewState.swift */, + 32B94DF2228EC26400716A26 /* ReactionsMenuViewAction.swift */, + 32B94DF3228EC26400716A26 /* ReactionsMenuButton.swift */, + ); + path = ReactionsMenu; + sourceTree = ""; + }; 32BF994D21FA1C6300698084 /* KeyBackup */ = { isa = PBXGroup; children = ( @@ -1884,6 +2017,31 @@ path = OnBoarding; sourceTree = ""; }; + B1963B24228F1C4800CBA17F /* BubbleReactions */ = { + isa = PBXGroup; + children = ( + 329E746522CD02EA006F9797 /* BubbleReactionActionViewCell.swift */, + 329E746422CD02EA006F9797 /* BubbleReactionActionViewCell.xib */, + B1963B31228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift */, + B1963B27228F1C4800CBA17F /* BubbleReactionsViewModel.swift */, + B1963B25228F1C4800CBA17F /* BubbleReactionsView.swift */, + B1963B2A228F1C4800CBA17F /* BubbleReactionsView.xib */, + B1963B28228F1C4800CBA17F /* BubbleReactionViewData.swift */, + B1963B29228F1C4800CBA17F /* BubbleReactionViewCell.swift */, + B1963B26228F1C4800CBA17F /* BubbleReactionViewCell.xib */, + ); + path = BubbleReactions; + sourceTree = ""; + }; + B1963B3622933B9500CBA17F /* CollectionView */ = { + isa = PBXGroup; + children = ( + B1963B3722933BC800CBA17F /* AutosizedCollectionView.swift */, + ); + name = CollectionView; + path = Riot/Modules/Common/CollectionView; + sourceTree = SOURCE_ROOT; + }; B1B5567620EE6C4C00210D55 /* Modules */ = { isa = PBXGroup; children = ( @@ -1985,6 +2143,7 @@ B1B5568E20EE6C4C00210D55 /* Room */ = { isa = PBXGroup; children = ( + 32A6000C22C661100042C1D9 /* EditHistory */, B1B5568F20EE6C4C00210D55 /* RoomViewController.h */, B1B556A020EE6C4C00210D55 /* RoomViewController.m */, B1B5569620EE6C4C00210D55 /* RoomViewController.xib */, @@ -1997,6 +2156,8 @@ B1B556A120EE6C4C00210D55 /* Files */, B1B556A420EE6C4C00210D55 /* Members */, B1B5569020EE6C4C00210D55 /* Settings */, + B1C562D7228C0B4C0037F12A /* ContextualMenu */, + B1963B24228F1C4800CBA17F /* BubbleReactions */, ); path = Room; sourceTree = ""; @@ -2178,6 +2339,7 @@ B1B556CD20EE6C4C00210D55 /* Common */ = { isa = PBXGroup; children = ( + B1963B3622933B9500CBA17F /* CollectionView */, B1B556CE20EE6C4C00210D55 /* WebViewController */, B1B556D120EE6C4C00210D55 /* NavigationController */, B1B556D420EE6C4C00210D55 /* SegmentedViewController */, @@ -2789,6 +2951,7 @@ B1B5584220EF768E00210D55 /* Encryption */ = { isa = PBXGroup; children = ( + B1A68592229E807800D6C09A /* RoomBubbleCellLayout.swift */, B1B5584320EF768E00210D55 /* RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.h */, B1B5584420EF768E00210D55 /* RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.h */, B1B5584520EF768E00210D55 /* RoomOutgoingEncryptedAttachmentBubbleCell.h */, @@ -2943,6 +3106,7 @@ B1B5598420EFC3DF00210D55 /* WidgetManager.m */, B1B5598120EFC3DF00210D55 /* Widget.h */, B1B5598320EFC3DF00210D55 /* Widget.m */, + 322C110722BBC6F80043FEAC /* WidgetManagerConfig.swift */, ); path = Widgets; sourceTree = ""; @@ -2977,6 +3141,23 @@ path = Analytics; sourceTree = ""; }; + B1C562D7228C0B4C0037F12A /* ContextualMenu */ = { + isa = PBXGroup; + children = ( + 32B94DF0228EC26400716A26 /* ReactionsMenu */, + B1C562DA228C0BB00037F12A /* RoomContextualMenuAction.swift */, + B1C562D8228C0B760037F12A /* RoomContextualMenuItem.swift */, + B1C562DE228C7C8B0037F12A /* RoomContextualMenuPresenter.swift */, + B1C562DD228C7C8A0037F12A /* RoomContextualMenuViewController.swift */, + B1C562E0228C7C8C0037F12A /* RoomContextualMenuViewController.storyboard */, + B1C562E6228C7CF10037F12A /* ContextualMenuItemView.swift */, + B1C562E7228C7CF20037F12A /* ContextualMenuItemView.xib */, + B1C562DC228C7C890037F12A /* RoomContextualMenuToolbarView.swift */, + B1C562DF228C7C8C0037F12A /* RoomContextualMenuToolbarView.xib */, + ); + path = ContextualMenu; + sourceTree = ""; + }; B1CE9EFB22148681000FAE6A /* SignOut */ = { isa = PBXGroup; children = ( @@ -3131,6 +3312,10 @@ B109D6F0222D8C400061B6D9 /* UIApplication.swift */, B1DB4F05223015080065DBFA /* Character.swift */, B1DB4F0A223131600065DBFA /* String.swift */, + B1A5B33D227ADF2A004CBA85 /* UIImage.swift */, + B1C562C92289C2690037F12A /* UIGestureRecognizer.swift */, + B1C562CB228AB3510037F12A /* UIStackView.swift */, + B1B12B2822942315002CB419 /* UITouch.swift */, ); path = Categories; sourceTree = ""; @@ -3292,7 +3477,7 @@ 24CBEC4D1F0EAD310093EABB = { CreatedOnToolsVersion = 8.3.2; DevelopmentTeam = 7J4U792NQT; - LastSwiftMigration = 1010; + LastSwiftMigration = 1020; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { @@ -3313,7 +3498,7 @@ F094A9A11B78D8F000B1FBBF = { CreatedOnToolsVersion = 6.2; DevelopmentTeam = 7J4U792NQT; - LastSwiftMigration = 1010; + LastSwiftMigration = 1020; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { @@ -3426,13 +3611,16 @@ B1B558EA20EF768F00210D55 /* RoomOutgoingTextMsgWithPaginationTitleBubbleCell.xib in Resources */, B1B558CD20EF768F00210D55 /* RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.xib in Resources */, B1B9194C2118984300FE25B5 /* RoomPredecessorBubbleCell.xib in Resources */, + B1C562E9228C7CF20037F12A /* ContextualMenuItemView.xib in Resources */, B1B5572120EE6C4D00210D55 /* ContactsTableViewController.xib in Resources */, B1B5593A20EF7BAC00210D55 /* TableViewCellWithLabelAndLargeTextView.xib in Resources */, B1B558D820EF768F00210D55 /* RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.xib in Resources */, B1B558DA20EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.xib in Resources */, B1B558D620EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */, B1B5593720EF7BAC00210D55 /* TableViewCellWithCheckBoxAndLabel.xib in Resources */, + B190F55B22CE35FD00AEB493 /* EditHistoryHeaderView.xib in Resources */, B1B5579020EF568D00210D55 /* GroupTableViewCell.xib in Resources */, + B142317B22CCFA2000FFA96A /* EditHistoryCell.xib in Resources */, B1B5581B20EF625800210D55 /* ExpandedRoomTitleView.xib in Resources */, B1B558C220EF768F00210D55 /* RoomIncomingEncryptedTextMsgBubbleCell.xib in Resources */, B1B558F620EF768F00210D55 /* RoomOutgoingTextMsgBubbleCell.xib in Resources */, @@ -3442,6 +3630,7 @@ B1B5574320EE6C4D00210D55 /* CallViewController.xib in Resources */, F083BDEA1E7009ED00A9B29C /* ringback.mp3 in Resources */, F083BDF21E7009ED00A9B29C /* GoogleService-Info.plist in Resources */, + 329E746622CD02EA006F9797 /* BubbleReactionActionViewCell.xib in Resources */, B1B558E320EF768F00210D55 /* RoomEmptyBubbleCell.xib in Resources */, B1E5368F21FB7258001F3AFF /* KeyBackupRecoverFromPassphraseViewController.storyboard in Resources */, B1B5590420EF768F00210D55 /* RoomOutgoingAttachmentBubbleCell.xib in Resources */, @@ -3462,6 +3651,7 @@ B1B5593920EF7BAC00210D55 /* TableViewCellWithCheckBoxes.xib in Resources */, B1B557C120EF5B4500210D55 /* DisabledRoomInputToolbarView.xib in Resources */, 32891D6C2264CBA300C82226 /* SimpleScreenTemplateViewController.storyboard in Resources */, + B1963B2C228F1C4900CBA17F /* BubbleReactionViewCell.xib in Resources */, B1664DA320F4F96200808783 /* Vector.strings in Resources */, B1B557C720EF5CD400210D55 /* DirectoryServerDetailTableViewCell.xib in Resources */, B1B5582620EF638A00210D55 /* RoomMemberTitleView.xib in Resources */, @@ -3475,6 +3665,7 @@ 3232AB1422564D9100AD6A5C /* swiftgen-config.yml in Resources */, 324A204F225FC571004FE8B0 /* DeviceVerificationIncomingViewController.storyboard in Resources */, 3232AB4B2256558300AD6A5C /* TemplateScreenViewController.storyboard in Resources */, + B1D1BDA822BBAFC900831367 /* ReactionsMenuView.xib in Resources */, 32B1FEDB21A46F2C00637127 /* TermsView.xib in Resources */, B1B5578E20EF568D00210D55 /* GroupInviteTableViewCell.xib in Resources */, B1B5582020EF625800210D55 /* SimpleRoomTitleView.xib in Resources */, @@ -3492,10 +3683,12 @@ B1B557C020EF5B4500210D55 /* RoomInputToolbarView.xib in Resources */, B1B5583D20EF6E7F00210D55 /* GroupRoomTableViewCell.xib in Resources */, B1B5572D20EE6C4D00210D55 /* RoomParticipantsViewController.xib in Resources */, + 32A6001C22C661100042C1D9 /* EditHistoryViewController.storyboard in Resources */, B1B5577220EE702800210D55 /* JitsiViewController.xib in Resources */, B1B557D720EF5EA900210D55 /* RoomActivitiesView.xib in Resources */, B1098BF821ECFE65000DDA48 /* KeyBackupSetupPassphraseViewController.storyboard in Resources */, F083BDF31E7009ED00A9B29C /* Images.xcassets in Resources */, + B1C562E4228C7C8D0037F12A /* RoomContextualMenuToolbarView.xib in Resources */, B1B5590720EF768F00210D55 /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */, B169329920F39E6300746532 /* LaunchScreen.storyboard in Resources */, B1B5595320EF9A8700210D55 /* RecentTableViewCell.xib in Resources */, @@ -3512,6 +3705,7 @@ B1B5590F20EF782800210D55 /* TableViewCellWithPhoneNumberTextField.xib in Resources */, B1B5578520EF564900210D55 /* GroupTableViewCellWithSwitch.xib in Resources */, B1B557B320EF5AEF00210D55 /* EventDetailsView.xib in Resources */, + B1963B30228F1C4900CBA17F /* BubbleReactionsView.xib in Resources */, B1B557DD20EF5FBB00210D55 /* FilesSearchTableViewCell.xib in Resources */, B1B5590320EF768F00210D55 /* RoomSelectedStickerBubbleCell.xib in Resources */, 3232ABB62257BE6400AD6A5C /* DeviceVerificationVerifyViewController.storyboard in Resources */, @@ -3520,6 +3714,7 @@ B1B558C020EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.xib in Resources */, B1B5572420EE6C4D00210D55 /* RoomViewController.xib in Resources */, B169331520F3CAFC00746532 /* PublicRoomTableViewCell.xib in Resources */, + B1C562E5228C7C8D0037F12A /* RoomContextualMenuViewController.storyboard in Resources */, 3232ABA2225730E100AD6A5C /* DeviceVerificationStartViewController.storyboard in Resources */, 3284A35120A07C210044F922 /* postMessageAPI.js in Resources */, B1B557A220EF58AD00210D55 /* ContactTableViewCell.xib in Resources */, @@ -3553,52 +3748,11 @@ files = ( ); inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-RiotPods-Riot/Pods-RiotPods-Riot-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework", - "${BUILT_PRODUCTS_DIR}/DTCoreText/DTCoreText.framework", - "${BUILT_PRODUCTS_DIR}/DTFoundation/DTFoundation.framework", - "${BUILT_PRODUCTS_DIR}/GBDeviceInfo/GBDeviceInfo.framework", - "${BUILT_PRODUCTS_DIR}/GZIP/GZIP.framework", - "${BUILT_PRODUCTS_DIR}/HPGrowingTextView/HPGrowingTextView.framework", - "${PODS_ROOT}/JitsiMeetSDK/Frameworks/JitsiMeet.framework", - "${PODS_ROOT}/JitsiMeetSDK/Frameworks/WebRTC.framework", - "${BUILT_PRODUCTS_DIR}/MatrixKit/MatrixKit.framework", - "${BUILT_PRODUCTS_DIR}/MatrixSDK.common-JingleCallStack/MatrixSDK.framework", - "${BUILT_PRODUCTS_DIR}/OLMKit/OLMKit.framework", - "${BUILT_PRODUCTS_DIR}/PiwikTracker/PiwikTracker.framework", - "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", - "${BUILT_PRODUCTS_DIR}/Reusable/Reusable.framework", - "${BUILT_PRODUCTS_DIR}/cmark/cmark.framework", - "${BUILT_PRODUCTS_DIR}/libPhoneNumber-iOS/libPhoneNumber_iOS.framework", - "${BUILT_PRODUCTS_DIR}/libbase58/libbase58.framework", - "${BUILT_PRODUCTS_DIR}/zxcvbn-ios/zxcvbn_ios.framework", - "${BUILT_PRODUCTS_DIR}/DTCoreText.default-Extension/DTCoreText.framework", - "${BUILT_PRODUCTS_DIR}/MatrixKit-AppExtension/MatrixKit.framework", + "${PODS_ROOT}/Target Support Files/Pods-RiotPods-Riot/Pods-RiotPods-Riot-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AFNetworking.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTCoreText.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DTFoundation.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GBDeviceInfo.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GZIP.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/HPGrowingTextView.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JitsiMeet.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MatrixKit.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MatrixSDK.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OLMKit.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PiwikTracker.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reusable.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cmark.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libPhoneNumber_iOS.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libbase58.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/zxcvbn_ios.framework", + "${PODS_ROOT}/Target Support Files/Pods-RiotPods-Riot/Pods-RiotPods-Riot-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -3795,6 +3949,7 @@ B1B558E820EF768F00210D55 /* RoomIncomingAttachmentWithPaginationTitleBubbleCell.m in Sources */, B1B558F320EF768F00210D55 /* RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m in Sources */, B1B557BD20EF5B4500210D55 /* KeyboardGrowingTextView.m in Sources */, + B1A68593229E807A00D6C09A /* RoomBubbleCellLayout.swift in Sources */, B1B558F420EF768F00210D55 /* RoomOutgoingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, B1B5572320EE6C4D00210D55 /* AttachmentsViewController.m in Sources */, F083BDEE1E7009ED00A9B29C /* MXRoom+Riot.m in Sources */, @@ -3805,9 +3960,11 @@ B1098BDF21ECE09F000DDA48 /* Strings.swift in Sources */, B1B558C420EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, 3232ABC022594C0900AD6A5C /* VerifyEmojiCollectionViewCell.swift in Sources */, + B1963B2E228F1C4900CBA17F /* BubbleReactionViewData.swift in Sources */, B1B5572F20EE6C4D00210D55 /* ReadReceiptsViewController.m in Sources */, B1B558CB20EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.m in Sources */, B169330B20F3CA3A00746532 /* Contact.m in Sources */, + B1A5B33E227ADF2A004CBA85 /* UIImage.swift in Sources */, B1D4752A21EE52B10067973F /* KeyBackupSetupIntroViewController.swift in Sources */, B1B5599220EFC5E400210D55 /* Analytics.m in Sources */, B14F143422144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewAction.swift in Sources */, @@ -3815,9 +3972,11 @@ B1B558C320EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithoutSenderNameBubbleCell.m 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 */, B139C21B21FE5B9200BB68EC /* KeyBackupRecoverFromPassphraseViewModel.swift in Sources */, + 32A6001E22C661100042C1D9 /* EditHistoryCoordinatorBridgePresenter.swift in Sources */, B1B5574A20EE6C4D00210D55 /* MediaPickerViewController.m in Sources */, B1B5598520EFC3E000210D55 /* RageShakeManager.m in Sources */, 3232ABA8225730E100AD6A5C /* DeviceVerificationStartViewState.swift in Sources */, @@ -3836,6 +3995,7 @@ B1B5593B20EF7BAC00210D55 /* TableViewCellWithCheckBoxAndLabel.m in Sources */, B1B5581A20EF625800210D55 /* ExpandedRoomTitleView.m in Sources */, B1107EC82200B0720038014B /* KeyBackupRecoverSuccessViewController.swift in Sources */, + B1963B2F228F1C4900CBA17F /* BubbleReactionViewCell.swift in Sources */, B1B558E920EF768F00210D55 /* RoomSelectedStickerBubbleCell.m in Sources */, B1B558DF20EF768F00210D55 /* RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m in Sources */, F083BE041E7009ED00A9B29C /* Tools.m in Sources */, @@ -3852,6 +4012,7 @@ 32891D712264DF7B00C82226 /* DeviceVerificationVerifiedViewController.swift in Sources */, F083BDEF1E7009ED00A9B29C /* UINavigationController+Riot.m in Sources */, B1B5581F20EF625800210D55 /* SimpleRoomTitleView.m in Sources */, + B1C562E2228C7C8D0037F12A /* RoomContextualMenuViewController.swift in Sources */, B169330020F3C97D00746532 /* RoomDataSource.m in Sources */, B1B558ED20EF768F00210D55 /* RoomIncomingTextMsgWithoutSenderNameBubbleCell.m in Sources */, B1B5571920EE6C4D00210D55 /* LanguagePickerViewController.m in Sources */, @@ -3862,19 +4023,23 @@ B1B558DD20EF768F00210D55 /* RoomIncomingEncryptedTextMsgBubbleCell.m in Sources */, B1098BE521ECE1FC000DDA48 /* Storyboards.swift in Sources */, 3232ABC2225B996200AD6A5C /* Themable.swift in Sources */, + 32A6001B22C661100042C1D9 /* EditHistoryViewAction.swift in Sources */, 3232ABA7225730E100AD6A5C /* DeviceVerificationStartCoordinator.swift in Sources */, B1D4752721EE4E630067973F /* KeyboardAvoider.swift in Sources */, B1D4752821EE4E630067973F /* KeyboardNotification.swift in Sources */, + B1D1BDA622BBAFB500831367 /* ReactionsMenuView.swift in Sources */, B1B5573C20EE6C4D00210D55 /* MasterTabBarController.m in Sources */, 32F6B96E2270623100BBA352 /* DeviceVerificationDataLoadingViewModelType.swift in Sources */, B1B5592C20EF7A5D00210D55 /* TableViewCellWithButton.m in Sources */, 32242F1421E8FBA900725742 /* DefaultTheme.swift in Sources */, + B1963B2D228F1C4900CBA17F /* BubbleReactionsViewModel.swift in Sources */, 32242F1321E8FBA900725742 /* Theme.swift in Sources */, B1B5582520EF638A00210D55 /* RoomMemberTitleView.m in Sources */, B1B5582C20EF666100210D55 /* DirectoryRecentTableViewCell.m in Sources */, B1B558E420EF768F00210D55 /* RoomMembershipWithPaginationTitleBubbleCell.m in Sources */, B1B5573620EE6C4D00210D55 /* GroupsViewController.m in Sources */, 3232ABB82257BE6500AD6A5C /* DeviceVerificationVerifyCoordinator.swift in Sources */, + B142317A22CCFA2000FFA96A /* EditHistoryCell.swift in Sources */, B1B5572A20EE6C4D00210D55 /* RoomMemberDetailsViewController.m in Sources */, B1B5590120EF768F00210D55 /* RoomMembershipExpandedWithPaginationTitleBubbleCell.m in Sources */, 32F6B96B2270623100BBA352 /* DeviceVerificationDataLoadingViewAction.swift in Sources */, @@ -3891,9 +4056,11 @@ 3232ABBA2257BE6500AD6A5C /* DeviceVerificationVerifyViewModel.swift in Sources */, B1098C1021ED07E4000DDA48 /* Presentable.swift in Sources */, B1B558E020EF768F00210D55 /* RoomOutgoingTextMsgBubbleCell.m in Sources */, + B1C562E3228C7C8D0037F12A /* RoomContextualMenuPresenter.swift in Sources */, B1B5593C20EF7BAC00210D55 /* TableViewCellWithCheckBoxes.m in Sources */, 32891D6B2264CBA300C82226 /* SimpleScreenTemplateViewController.swift in Sources */, B1CA3A2721EF6914000D1D89 /* UIViewController.swift in Sources */, + 322C110822BBC6F80043FEAC /* WidgetManagerConfig.swift in Sources */, F0D2ADA11F6AA5FD00A7097D /* MXRoomSummary+Riot.m in Sources */, B1B5596F20EFA85D00210D55 /* EncryptionInfoView.m in Sources */, B1B5573820EE6C4D00210D55 /* GroupParticipantsViewController.m in Sources */, @@ -3906,6 +4073,7 @@ B139C22121FE5D9D00BB68EC /* KeyBackupRecoverFromPassphraseViewState.swift in Sources */, B1B5579120EF568D00210D55 /* GroupInviteTableViewCell.m in Sources */, B1B5579A20EF575B00210D55 /* ForgotPasswordInputsView.m in Sources */, + B1B12B2922942315002CB419 /* UITouch.swift in Sources */, B1B558CC20EF768F00210D55 /* RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.m in Sources */, B1B5571D20EE6C4D00210D55 /* HomeViewController.m in Sources */, 3232ABA6225730E100AD6A5C /* DeviceVerificationStartViewController.swift in Sources */, @@ -3922,6 +4090,7 @@ B1B5572620EE6C4D00210D55 /* RoomFilesSearchViewController.m in Sources */, B1B5583120EF66BA00210D55 /* RoomIdOrAliasTableViewCell.m in Sources */, B1CA3A2921EF692B000D1D89 /* UIView.swift in Sources */, + 32A6001D22C661100042C1D9 /* EditHistoryCoordinatorType.swift in Sources */, F083BDFA1E7009ED00A9B29C /* RoomPreviewData.m in Sources */, B1B557B420EF5AEF00210D55 /* EventDetailsView.m in Sources */, B1B5577E20EE84BF00210D55 /* IncomingCallView.m in Sources */, @@ -3936,7 +4105,10 @@ B1B5572020EE6C4D00210D55 /* ContactsTableViewController.m in Sources */, B1B5581920EF625800210D55 /* RoomTitleView.m in Sources */, B1098BE321ECE09F000DDA48 /* RiotDefaults.swift in Sources */, + B1C562CA2289C2690037F12A /* UIGestureRecognizer.swift in Sources */, + B1C562CC228AB3510037F12A /* UIStackView.swift in Sources */, B1B557BE20EF5B4500210D55 /* RoomInputToolbarView.m in Sources */, + 32A6001922C661100042C1D9 /* EditHistoryViewModelType.swift in Sources */, B1B5573B20EE6C4D00210D55 /* FavouritesViewController.m in Sources */, B1B5579920EF575B00210D55 /* AuthInputsView.m in Sources */, B1B5597520EFB02A00210D55 /* InviteRecentTableViewCell.m in Sources */, @@ -3944,10 +4116,14 @@ B1798302211B13B3001FD722 /* OnBoardingManager.swift in Sources */, B1B5573520EE6C4D00210D55 /* GroupDetailsViewController.m in Sources */, B10B3B5B2201DD740072C76B /* KeyBackupBannerCell.swift in Sources */, + B1963B32228F1C6B00CBA17F /* BubbleReactionsViewModelType.swift in Sources */, + 32A6001722C661100042C1D9 /* EditHistoryViewController.swift in Sources */, B1098BFA21ECFE65000DDA48 /* KeyBackupSetupPassphraseViewModel.swift in Sources */, B1B5575220EE6C4D00210D55 /* RoomKeyRequestViewController.m in Sources */, + 32A6001A22C661100042C1D9 /* EditHistoryCoordinator.swift in Sources */, F083BD1E1E7009ED00A9B29C /* AppDelegate.m in Sources */, B1B558E620EF768F00210D55 /* RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m in Sources */, + 329E746722CD02EA006F9797 /* BubbleReactionActionViewCell.swift in Sources */, B1098BFB21ECFE65000DDA48 /* KeyBackupSetupCoordinatorType.swift in Sources */, B1098BF721ECFE65000DDA48 /* PasswordStrength.swift in Sources */, 324A2052225FC571004FE8B0 /* DeviceVerificationIncomingViewAction.swift in Sources */, @@ -3955,11 +4131,13 @@ B1B557D820EF5EA900210D55 /* RoomActivitiesView.m in Sources */, B1B5596620EF9E9B00210D55 /* RoomTableViewCell.m in Sources */, B14F143322144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModel.swift in Sources */, + 32A6001822C661100042C1D9 /* EditHistoryViewModel.swift in Sources */, B1B558D020EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, B1B558CF20EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.m in Sources */, B140B4A221F87F7100E3F5FE /* OperationQueue.swift in Sources */, B1B5575120EE6C4D00210D55 /* AuthenticationViewController.m in Sources */, B1B5571820EE6C4D00210D55 /* CountryPickerViewController.m in Sources */, + B1D211E622C194A200D939BD /* ReactionsMenuViewState.swift in Sources */, B17982FF2119FED2001FD722 /* GDPRConsentViewController.swift in Sources */, B1098BE121ECE09F000DDA48 /* Images.swift in Sources */, 3232ABA4225730E100AD6A5C /* DeviceVerificationStartViewAction.swift in Sources */, @@ -3977,11 +4155,14 @@ 324A2054225FC571004FE8B0 /* DeviceVerificationIncomingCoordinatorType.swift in Sources */, 3232ABB92257BE6500AD6A5C /* DeviceVerificationVerifyViewController.swift in Sources */, B139C21F21FE5D6600BB68EC /* KeyBackupRecoverFromPassphraseViewAction.swift in Sources */, + B1C562DB228C0BB00037F12A /* RoomContextualMenuAction.swift in Sources */, B1B5574720EE6C4D00210D55 /* UsersDevicesViewController.m in Sources */, B1098BFF21ECFE65000DDA48 /* PasswordStrengthView.swift in Sources */, B1B558D220EF768F00210D55 /* RoomEncryptedDataBubbleCell.m in Sources */, B1B558FA20EF768F00210D55 /* RoomMembershipBubbleCell.m in Sources */, 3232ABA1225730E100AD6A5C /* DeviceVerificationCoordinatorType.swift in Sources */, + B1C562D9228C0B760037F12A /* RoomContextualMenuItem.swift in Sources */, + B1C562E1228C7C8C0037F12A /* RoomContextualMenuToolbarView.swift in Sources */, B1B557BF20EF5B4500210D55 /* DisabledRoomInputToolbarView.m in Sources */, B1B5578620EF564900210D55 /* GroupTableViewCellWithSwitch.m in Sources */, B1098BE821ECFE52000DDA48 /* Coordinator.swift in Sources */, @@ -3994,8 +4175,10 @@ B110872621F098F0003554A5 /* ActivityIndicatorView.swift in Sources */, B19EFA3921F8BB2C00FC070E /* KeyBackupRecoverCoordinatorType.swift in Sources */, B1E5368D21FB7245001F3AFF /* KeyBackupRecoverFromPassphraseViewController.swift in Sources */, + B1963B3822933BC800CBA17F /* AutosizedCollectionView.swift in Sources */, B169330320F3C98900746532 /* RoomBubbleCellData.m in Sources */, B1B557CC20EF5D8000210D55 /* DirectoryServerTableViewCell.m in Sources */, + B1963B2B228F1C4900CBA17F /* BubbleReactionsView.swift in Sources */, B1B5575C20EE6C4D00210D55 /* DirectoryViewController.m in Sources */, B1B558BD20EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.m in Sources */, B1B5577020EE702800210D55 /* WidgetPickerViewController.m in Sources */, @@ -4006,18 +4189,24 @@ B16932EE20F3C3C900746532 /* FilesSearchCellData.m in Sources */, B1B558E520EF768F00210D55 /* RoomMembershipExpandedBubbleCell.m in Sources */, 32BF995121FA29DC00698084 /* SettingsKeyBackupViewModelType.swift in Sources */, + B190F55922CE356800AEB493 /* EditHistoryHeaderView.swift in Sources */, 32F6B96A2270623100BBA352 /* DeviceVerificationDataLoadingViewState.swift in Sources */, 32BF995321FA2A1300698084 /* SettingsKeyBackupViewState.swift in Sources */, + 32B94DF9228EC26400716A26 /* ReactionsMenuViewAction.swift in Sources */, B1B5599420EFC5E400210D55 /* DecryptionFailureTracker.m in Sources */, F083BDF01E7009ED00A9B29C /* UIViewController+RiotSearch.m in Sources */, F083BDF91E7009ED00A9B29C /* RoomEmailInvitation.m in Sources */, + B1D211E422C18E3800D939BD /* ReactionsMenuViewModelType.swift in Sources */, 324A2055225FC571004FE8B0 /* DeviceVerificationIncomingViewModelType.swift in Sources */, B1B5572C20EE6C4D00210D55 /* RoomParticipantsViewController.m in Sources */, B1B558EE20EF768F00210D55 /* RoomOutgoingAttachmentBubbleCell.m in Sources */, 3232ABB52257BE6400AD6A5C /* DeviceVerificationVerifyCoordinatorType.swift in Sources */, 32BF994F21FA29A400698084 /* SettingsKeyBackupViewModel.swift in Sources */, + B190F55D22CE5A9700AEB493 /* EditHistorySection.swift in Sources */, + 32A6002022C66FCF0042C1D9 /* EditHistoryMessage.swift in Sources */, B1B5574920EE6C4D00210D55 /* RiotSplitViewController.m in Sources */, B1B5574E20EE6C4D00210D55 /* DirectoryServerPickerViewController.m in Sources */, + B1D211E822C195B400D939BD /* ReactionMenuItemViewData.swift in Sources */, B1DB4F0B223131600065DBFA /* String.swift in Sources */, 3232AB522256558300AD6A5C /* TemplateScreenViewModel.swift in Sources */, B1B5575B20EE6C4D00210D55 /* HomeFilesSearchViewController.m in Sources */, @@ -4036,8 +4225,11 @@ B1098C0021ECFE65000DDA48 /* KeyBackupSetupPassphraseViewController.swift in Sources */, B1B5591020EF782800210D55 /* TableViewCellWithPhoneNumberTextField.m in Sources */, B1DB4F06223015080065DBFA /* Character.swift in Sources */, + 32B94DFA228EC26400716A26 /* ReactionsMenuButton.swift in Sources */, + B1C562E8228C7CF20037F12A /* ContextualMenuItemView.swift in Sources */, B14F143022144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyCoordinatorType.swift in Sources */, B1E5368921FB1E20001F3AFF /* UIButton.swift in Sources */, + 32A6001622C661100042C1D9 /* EditHistoryViewState.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4190,7 +4382,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/$(PRODUCT_NAME)/SupportingFiles/RiotShareExtension-Bridging-Header.h"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; USER_HEADER_SEARCH_PATHS = "$(inherited)"; }; name = Debug; @@ -4230,7 +4422,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/$(PRODUCT_NAME)/SupportingFiles/RiotShareExtension-Bridging-Header.h"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; USER_HEADER_SEARCH_PATHS = "$(inherited)"; }; name = Release; @@ -4433,7 +4625,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/$(PRODUCT_NAME)/SupportingFiles/Riot-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; USER_HEADER_SEARCH_PATHS = "$(inherited)"; }; @@ -4464,7 +4656,7 @@ PRODUCT_BUNDLE_IDENTIFIER = im.vector.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/$(PRODUCT_NAME)/SupportingFiles/Riot-Bridging-Header.h"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; USER_HEADER_SEARCH_PATHS = "$(inherited)"; }; diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m index 65b7ffdd3f..db380b2ec9 100644 --- a/Riot/AppDelegate.m +++ b/Riot/AppDelegate.m @@ -2324,6 +2324,7 @@ - (BOOL)handleUniversalLinkFragment:(NSString*)fragment [homeViewController stopActivityIndicator]; roomPreviewData = [[RoomPreviewData alloc] initWithRoomId:roomIdOrAlias emailInvitationParams:queryParams andSession:account.mxSession]; + roomPreviewData.viaServers = queryParams[@"via"]; [self showRoomPreview:roomPreviewData]; } else @@ -2522,8 +2523,22 @@ - (void)parseUniversalLinkFragment:(NSString*)fragment outPathParams:(NSArrayhttps://github.com/matrix-org/matrix-ios-kit.git)

The Matrix reusable UI library for iOS based on MatrixSDK.

Copyright (c) 2014-2016 OpenMarket Ltd. +
Copyright (c) 2016-2017 Vector Creations Ltd +
Copyright (c) 2018-2019 New Vector Ltd +
Copyright (c) 2019 The Matrix.org Foundation C.I.C

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License at:

https://www.apache.org/licenses/LICENSE-2.0 @@ -33,6 +36,9 @@

The iOS SDK to build apps compatible with Matrix (https://www.matrix.org).

Copyright (c) 2014-2016 OpenMarket Ltd. +
Copyright (c) 2016-2017 Vector Creations Ltd +
Copyright (c) 2018-2019 New Vector Ltd +
Copyright (c) 2019 The Matrix.org Foundation C.I.C

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License at:

https://www.apache.org/licenses/LICENSE-2.0 @@ -131,951 +137,965 @@

  • - WebRTC-iOS (WebRTC iOS framework) -

    webrtc

    -
    -            Copyright (c) 2011, The WebRTC project authors. All rights reserved.
    -
    -            Redistribution and use in source and binary forms, with or without
    -            modification, are permitted provided that the following conditions are
    -            met:
    -
    -            * Redistributions of source code must retain the above copyright
    -            notice, this list of conditions and the following disclaimer.
    -
    -            * 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.
    -
    -            * Neither the name of Google 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.
    -
    -            This source tree contains third party source code which is governed by third
    -            party licenses. Paths to the files and associated licenses are collected here.
    -
    -            Files governed by third party licenses:
    -            common_audio/fft4g.c
    -            common_audio/signal_processing/spl_sqrt_floor.c
    -            common_audio/signal_processing/spl_sqrt_floor_arm.S
    -            modules/audio_coding/codecs/g711/main/source/g711.c
    -            modules/audio_coding/codecs/g711/main/source/g711.h
    -            modules/audio_coding/codecs/g722/main/source/g722_decode.c
    -            modules/audio_coding/codecs/g722/main/source/g722_enc_dec.h
    -            modules/audio_coding/codecs/g722/main/source/g722_encode.c
    -            modules/audio_coding/codecs/isac/main/source/fft.c
    -            modules/audio_device/mac/portaudio/pa_memorybarrier.h
    -            modules/audio_device/mac/portaudio/pa_ringbuffer.c
    -            modules/audio_device/mac/portaudio/pa_ringbuffer.h
    -            modules/audio_processing/aec/aec_rdft.c
    -            system_wrappers/source/condition_variable_event_win.cc
    -            system_wrappers/source/set_thread_name_win.h
    -            system_wrappers/source/spreadsortlib/constants.hpp
    -            system_wrappers/source/spreadsortlib/spreadsort.hpp
    -
    -            Individual licenses for each file:
    -            -------------------------------------------------------------------------------
    -            Files:
    -            common_audio/signal_processing/spl_sqrt_floor.c
    -            common_audio/signal_processing/spl_sqrt_floor_arm.S
    -
    -            License:
    -            /*
    -            * Written by Wilco Dijkstra, 1996. The following email exchange establishes the
    -            * license.
    -            *
    -            * From: Wilco Dijkstra <Wilco.Dijkstra@ntlworld.com>
    -            * Date: Fri, Jun 24, 2011 at 3:20 AM
    -            * Subject: Re: sqrt routine
    -            * To: Kevin Ma <kma@google.com>
    -            * Hi Kevin,
    -            * Thanks for asking. Those routines are public domain (originally posted to
    -            * comp.sys.arm a long time ago), so you can use them freely for any purpose.
    -            * Cheers,
    -            * Wilco
    -            *
    -            * ----- Original Message -----
    -            * From: "Kevin Ma" <kma@google.com>
    -            * To: <Wilco.Dijkstra@ntlworld.com>
    -            * Sent: Thursday, June 23, 2011 11:44 PM
    -            * Subject: Fwd: sqrt routine
    -            * Hi Wilco,
    -            * I saw your sqrt routine from several web sites, including
    -            * http://www.finesse.demon.co.uk/steven/sqrt.html.
    -            * Just wonder if there's any copyright information with your Successive
    -            * approximation routines, or if I can freely use it for any purpose.
    -            * Thanks.
    -            * Kevin
    -            */
    -            -------------------------------------------------------------------------------
    -            Files:
    -            modules/audio_coding/codecs/g711/main/source/g711.c
    -            modules/audio_coding/codecs/g711/main/source/g711.h
    -
    -            License:
    -            /*
    -            * SpanDSP - a series of DSP components for telephony
    -            *
    -            * g711.h - In line A-law and u-law conversion routines
    -            *
    -            * Written by Steve Underwood <steveu@coppice.org>
    -            *
    -            * Copyright (C) 2001 Steve Underwood
    -            *
    -            *  Despite my general liking of the GPL, I place this code in the
    -            *  public domain for the benefit of all mankind - even the slimy
    -            *  ones who might try to proprietize my work and use it to my
    -            *  detriment.
    -            */
    -            -------------------------------------------------------------------------------
    -            Files:
    -            modules/audio_coding/codecs/g722/main/source/g722_decode.c
    -            modules/audio_coding/codecs/g722/main/source/g722_enc_dec.h
    -            modules/audio_coding/codecs/g722/main/source/g722_encode.c
    -
    -            License:
    -            /*
    -            * SpanDSP - a series of DSP components for telephony
    -            *
    -            * g722_decode.c - The ITU G.722 codec, decode part.
    -            *
    -            * Written by Steve Underwood <steveu@coppice.org>
    -            *
    -            * Copyright (C) 2005 Steve Underwood
    -            *
    -            *  Despite my general liking of the GPL, I place my own contributions
    -            *  to this code in the public domain for the benefit of all mankind -
    -            *  even the slimy ones who might try to proprietize my work and use it
    -            *  to my detriment.
    -            *
    -            * Based in part on a single channel G.722 codec which is:
    -            *
    -            * Copyright (c) CMU 1993
    -            * Computer Science, Speech Group
    -            * Chengxiang Lu and Alex Hauptmann
    -            */
    -            -------------------------------------------------------------------------------
    -            Files:
    -            modules/audio_coding/codecs/isac/main/source/fft.c
    -
    -            License:
    -            /*
    -            * Copyright(c)1995,97 Mark Olesen <olesen@me.QueensU.CA>
    -            *    Queen's Univ at Kingston (Canada)
    -            *
    -            * Permission to use, copy, modify, and distribute this software for
    -            * any purpose without fee is hereby granted, provided that this
    -            * entire notice is included in all copies of any software which is
    -            * or includes a copy or modification of this software and in all
    -            * copies of the supporting documentation for such software.
    -            *
    -            * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR
    -            * IMPLIED WARRANTY.  IN PARTICULAR, NEITHER THE AUTHOR NOR QUEEN'S
    -            * UNIVERSITY AT KINGSTON MAKES ANY REPRESENTATION OR WARRANTY OF ANY
    -            * KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS
    -            * FITNESS FOR ANY PARTICULAR PURPOSE.
    -            *
    -            * All of which is to say that you can do what you like with this
    -            * source code provided you don't try to sell it as your own and you
    -            * include an unaltered copy of this message (including the
    -            * copyright).
    -            *
    -            * It is also implicitly understood that bug fixes and improvements
    -            * should make their way back to the general Internet community so
    -            * that everyone benefits.
    -            */
    -            -------------------------------------------------------------------------------
    -            Files:
    -            modules/audio_device/mac/portaudio/pa_memorybarrier.h
    -            modules/audio_device/mac/portaudio/pa_ringbuffer.c
    -            modules/audio_device/mac/portaudio/pa_ringbuffer.h
    -
    -            License:
    -            /*
    -            * $Id: pa_memorybarrier.h 1240 2007-07-17 13:05:07Z bjornroche $
    -            * Portable Audio I/O Library
    -            * Memory barrier utilities
    -            *
    -            * Author: Bjorn Roche, XO Audio, LLC
    -            *
    -            * This program uses the PortAudio Portable Audio Library.
    -            * For more information see: http://www.portaudio.com
    -            * Copyright (c) 1999-2000 Ross Bencina and Phil Burk
    -            *
    -            * Permission is hereby granted, free of charge, to any person obtaining
    -            * a copy of this software and associated documentation files
    -            * (the "Software"), to deal in the Software without restriction,
    -            * including without limitation the rights to use, copy, modify, merge,
    -            * publish, distribute, sublicense, and/or sell copies of the Software,
    -            * and to permit persons to whom the Software is furnished to do so,
    -            * subject to the following conditions:
    -            *
    -            * The above copyright notice and this permission notice shall be
    -            * included in all copies or substantial portions of the Software.
    -            *
    -            * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    -            * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    -            * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    -            * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
    -            * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
    -            * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
    -            * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    -            */
    -
    -            /*
    -            * The text above constitutes the entire PortAudio license; however,
    -            * the PortAudio community also makes the following non-binding requests:
    -            *
    -            * Any person wishing to distribute modifications to the Software is
    -            * requested to send the modifications to the original developer so that
    -            * they can be incorporated into the canonical version. It is also
    -            * requested that these non-binding requests be included along with the
    -            * license above.
    -            */
    -
    -            /*
    -            * $Id: pa_ringbuffer.c 1421 2009-11-18 16:09:05Z bjornroche $
    -            * Portable Audio I/O Library
    -            * Ring Buffer utility.
    -            *
    -            * Author: Phil Burk, http://www.softsynth.com
    -            * modified for SMP safety on Mac OS X by Bjorn Roche
    -            * modified for SMP safety on Linux by Leland Lucius
    -            * also, allowed for const where possible
    -            * modified for multiple-byte-sized data elements by Sven Fischer
    -            *
    -            * Note that this is safe only for a single-thread reader and a
    -            * single-thread writer.
    -            *
    -            * This program uses the PortAudio Portable Audio Library.
    -            * For more information see: http://www.portaudio.com
    -            * Copyright (c) 1999-2000 Ross Bencina and Phil Burk
    -            *
    -            * Permission is hereby granted, free of charge, to any person obtaining
    -            * a copy of this software and associated documentation files
    -            * (the "Software"), to deal in the Software without restriction,
    -            * including without limitation the rights to use, copy, modify, merge,
    -            * publish, distribute, sublicense, and/or sell copies of the Software,
    -            * and to permit persons to whom the Software is furnished to do so,
    -            * subject to the following conditions:
    -            *
    -            * The above copyright notice and this permission notice shall be
    -            * included in all copies or substantial portions of the Software.
    -            *
    -            * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    -            * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    -            * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    -            * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
    -            * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
    -            * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
    -            * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    -            */
    -
    -            /*
    -            * The text above constitutes the entire PortAudio license; however,
    -            * the PortAudio community also makes the following non-binding requests:
    -            *
    -            * Any person wishing to distribute modifications to the Software is
    -            * requested to send the modifications to the original developer so that
    -            * they can be incorporated into the canonical version. It is also
    -            * requested that these non-binding requests be included along with the
    -            * license above.
    -            */
    -            -------------------------------------------------------------------------------
    -            Files:
    -            common_audio/fft4g.c
    -            modules/audio_processing/aec/aec_rdft.c
    -
    -            License:
    -            /*
    -            * http://www.kurims.kyoto-u.ac.jp/~ooura/fft.html
    -            * Copyright Takuya OOURA, 1996-2001
    -            *
    -            * You may use, copy, modify and distribute this code for any purpose (include
    -            * commercial use) and without fee. Please refer to this package when you modify
    -            * this code.
    -            */
    -            -------------------------------------------------------------------------------
    -            Files:
    -            system_wrappers/source/condition_variable_event_win.cc
    -
    -            Source:
    -            http://www1.cse.wustl.edu/~schmidt/ACE-copying.html
    -
    -            License:
    -            Copyright and Licensing Information for ACE(TM), TAO(TM), CIAO(TM), DAnCE(TM),
    -            and CoSMIC(TM)
    -
    -            ACE(TM), TAO(TM), CIAO(TM), DAnCE>(TM), and CoSMIC(TM) (henceforth referred to
    -            as "DOC software") are copyrighted by Douglas C. Schmidt and his research
    -            group at Washington University, University of California, Irvine, and
    -            Vanderbilt University, Copyright (c) 1993-2009, all rights reserved. Since DOC
    -            software is open-source, freely available software, you are free to use,
    -            modify, copy, and distribute--perpetually and irrevocably--the DOC software
    -            source code and object code produced from the source, as well as copy and
    -            distribute modified versions of this software. You must, however, include this
    -            copyright statement along with any code built using DOC software that you
    -            release. No copyright statement needs to be provided if you just ship binary
    -            executables of your software products.
    -            You can use DOC software in commercial and/or binary software releases and are
    -            under no obligation to redistribute any of your source code that is built
    -            using DOC software. Note, however, that you may not misappropriate the DOC
    -            software code, such as copyrighting it yourself or claiming authorship of the
    -            DOC software code, in a way that will prevent DOC software from being
    -            distributed freely using an open-source development model. You needn't inform
    -            anyone that you're using DOC software in your software, though we encourage
    -            you to let us know so we can promote your project in the DOC software success
    -            stories.
    -
    -            The ACE, TAO, CIAO, DAnCE, and CoSMIC web sites are maintained by the DOC
    -            Group at the Institute for Software Integrated Systems (ISIS) and the Center
    -            for Distributed Object Computing of Washington University, St. Louis for the
    -            development of open-source software as part of the open-source software
    -            community. Submissions are provided by the submitter ``as is'' with no
    -            warranties whatsoever, including any warranty of merchantability,
    -            noninfringement of third party intellectual property, or fitness for any
    -            particular purpose. In no event shall the submitter be liable for any direct,
    -            indirect, special, exemplary, punitive, or consequential damages, including
    -            without limitation, lost profits, even if advised of the possibility of such
    -            damages. Likewise, DOC software is provided as is with no warranties of any
    -            kind, including the warranties of design, merchantability, and fitness for a
    -            particular purpose, noninfringement, or arising from a course of dealing,
    -            usage or trade practice. Washington University, UC Irvine, Vanderbilt
    -            University, their employees, and students shall have no liability with respect
    -            to the infringement of copyrights, trade secrets or any patents by DOC
    -            software or any part thereof. Moreover, in no event will Washington
    -            University, UC Irvine, or Vanderbilt University, their employees, or students
    -            be liable for any lost revenue or profits or other special, indirect and
    -            consequential damages.
    -
    -            DOC software is provided with no support and without any obligation on the
    -            part of Washington University, UC Irvine, Vanderbilt University, their
    -            employees, or students to assist in its use, correction, modification, or
    -            enhancement. A number of companies around the world provide commercial support
    -            for DOC software, however. DOC software is Y2K-compliant, as long as the
    -            underlying OS platform is Y2K-compliant. Likewise, DOC software is compliant
    -            with the new US daylight savings rule passed by Congress as "The Energy Policy
    -            Act of 2005," which established new daylight savings times (DST) rules for the
    -            United States that expand DST as of March 2007. Since DOC software obtains
    -            time/date and calendaring information from operating systems users will not be
    -            affected by the new DST rules as long as they upgrade their operating systems
    -            accordingly.
    -
    -            The names ACE(TM), TAO(TM), CIAO(TM), DAnCE(TM), CoSMIC(TM), Washington
    -            University, UC Irvine, and Vanderbilt University, may not be used to endorse
    -            or promote products or services derived from this source without express
    -            written permission from Washington University, UC Irvine, or Vanderbilt
    -            University. This license grants no permission to call products or services
    -            derived from this source ACE(TM), TAO(TM), CIAO(TM), DAnCE(TM), or CoSMIC(TM),
    -            nor does it grant permission for the name Washington University, UC Irvine, or
    -            Vanderbilt University to appear in their names.
    -            -------------------------------------------------------------------------------
    -            Files:
    -            system_wrappers/source/set_thread_name_win.h
    -
    -            Source:
    -            http://msdn.microsoft.com/en-us/cc300389.aspx#P
    -
    -            License:
    -            This license governs use of code marked as “sample” or “example” available on
    -            this web site without a license agreement, as provided under the section above
    -            titled “NOTICE SPECIFIC TO SOFTWARE AVAILABLE ON THIS WEB SITE.” If you use
    -            such code (the “software”), you accept this license. If you do not accept the
    -            license, do not use the software.
    -
    -            1. Definitions
    -
    -            The terms “reproduce,” “reproduction,” “derivative works,” and “distribution”
    -            have the same meaning here as under U.S. copyright law.
    -
    -            A “contribution” is the original software, or any additions or changes to the
    -            software.
    -
    -            A “contributor” is any person that distributes its contribution under this
    -            license.
    -
    -            “Licensed patents” are a contributor’s patent claims that read directly on its
    -            contribution.
    -
    -            2. Grant of Rights
    -
    -            (A) Copyright Grant - Subject to the terms of this license, including the
    -            license conditions and limitations in section 3, each contributor grants you a
    -            non-exclusive, worldwide, royalty-free copyright license to reproduce its
    -            contribution, prepare derivative works of its contribution, and distribute its
    -            contribution or any derivative works that you create.
    -
    -            (B) Patent Grant - Subject to the terms of this license, including the license
    -            conditions and limitations in section 3, each contributor grants you a
    -            non-exclusive, worldwide, royalty-free license under its licensed patents to
    -            make, have made, use, sell, offer for sale, import, and/or otherwise dispose
    -            of its contribution in the software or derivative works of the contribution in
    -            the software.
    -
    -            3. Conditions and Limitations
    -
    -            (A) No Trademark License- This license does not grant you rights to use any
    -            contributors’ name, logo, or trademarks.
    -
    -            (B) If you bring a patent claim against any contributor over patents that you
    -            claim are infringed by the software, your patent license from such contributor
    -            to the software ends automatically.
    -
    -            (C) If you distribute any portion of the software, you must retain all
    -            copyright, patent, trademark, and attribution notices that are present in the
    -            software.
    -
    -            (D) If you distribute any portion of the software in source code form, you may
    -            do so only under this license by including a complete copy of this license
    -            with your distribution. If you distribute any portion of the software in
    -            compiled or object code form, you may only do so under a license that complies
    -            with this license.
    -
    -            (E) The software is licensed “as-is.” You bear the risk of using it. The
    -            contributors give no express warranties, guarantees or conditions. You may
    -            have additional consumer rights under your local laws which this license
    -            cannot change. To the extent permitted under your local laws, the contributors
    -            exclude the implied warranties of merchantability, fitness for a particular
    -            purpose and non-infringement.
    -
    -            (F) Platform Limitation - The licenses granted in sections 2(A) and 2(B)
    -            extend only to the software or derivative works that you create that run on a
    -            Microsoft Windows operating system product.
    -            -------------------------------------------------------------------------------
    -            Files:
    -            system_wrappers/source/spreadsortlib/constants.hpp
    -            system_wrappers/source/spreadsortlib/spreadsort.hpp
    -
    -            License:
    -            /*Boost Software License - Version 1.0 - August 17th, 2003
    -
    -            Permission is hereby granted, free of charge, to any person or organization
    -            obtaining a copy of the software and accompanying documentation covered by
    -            this license (the "Software") to use, reproduce, display, distribute,
    -            execute, and transmit the Software, and to prepare derivative works of the
    -            Software, and to permit third-parties to whom the Software is furnished to
    -            do so, all subject to the following:
    -
    -            The copyright notices in the Software and this entire statement, including
    -            the above license grant, this restriction and the following disclaimer,
    -            must be included in all copies of the Software, in whole or in part, and
    -            all derivative works of the Software, unless such copies or derivative
    -            works are solely in the form of machine-executable object code generated by
    -            a source language processor.
    -
    -            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    -            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    -            FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
    -            SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
    -            FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
    -            ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    -            DEALINGS IN THE SOFTWARE.*/
    -
    -        
    -

    boringssl

    -
    -            BoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL
    -            licensing. Files that are completely new have a Google copyright and an ISC
    -            license. This license is reproduced at the bottom of this file.
    -
    -            Contributors to BoringSSL are required to follow the CLA rules for Chromium:
    -            https://cla.developers.google.com/clas
    -
    -            Some files from Intel are under yet another license, which is also included
    -            underneath.
    -
    -            The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the
    -            OpenSSL License and the original SSLeay license apply to the toolkit. See below
    -            for the actual license texts. Actually both licenses are BSD-style Open Source
    -            licenses. In case of any license issues related to OpenSSL please contact
    -            openssl-core@openssl.org.
    -
    -            The following are Google-internal bug numbers where explicit permission from
    -            some authors is recorded for use of their work. (This is purely for our own
    -            record keeping.)
    -            27287199
    -            27287880
    -            27287883
    -
    -            OpenSSL License
    -            ---------------
    -
    -            /* ====================================================================
    -            * Copyright (c) 1998-2011 The OpenSSL Project.  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. All advertising materials mentioning features or use of this
    -            *    software must display the following acknowledgment:
    -            *    "This product includes software developed by the OpenSSL Project
    -            *    for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
    -            *
    -            * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
    -            *    endorse or promote products derived from this software without
    -            *    prior written permission. For written permission, please contact
    -            *    openssl-core@openssl.org.
    -            *
    -            * 5. Products derived from this software may not be called "OpenSSL"
    -            *    nor may "OpenSSL" appear in their names without prior written
    -            *    permission of the OpenSSL Project.
    -            *
    -            * 6. Redistributions of any form whatsoever must retain the following
    -            *    acknowledgment:
    -            *    "This product includes software developed by the OpenSSL Project
    -            *    for use in the OpenSSL Toolkit (http://www.openssl.org/)"
    -            *
    -            * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
    -            * EXPRESSED 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 OpenSSL PROJECT OR
    -            * ITS 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.
    -            * ====================================================================
    -            *
    -            * This product includes cryptographic software written by Eric Young
    -            * (eay@cryptsoft.com).  This product includes software written by Tim
    -            * Hudson (tjh@cryptsoft.com).
    -            *
    -            */
    -
    -            Original SSLeay License
    -            -----------------------
    -
    -            /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
    -            * All rights reserved.
    -            *
    -            * This package is an SSL implementation written
    -            * by Eric Young (eay@cryptsoft.com).
    -            * The implementation was written so as to conform with Netscapes SSL.
    -            *
    -            * This library is free for commercial and non-commercial use as long as
    -            * the following conditions are aheared to.  The following conditions
    -            * apply to all code found in this distribution, be it the RC4, RSA,
    -            * lhash, DES, etc., code; not just the SSL code.  The SSL documentation
    -            * included with this distribution is covered by the same copyright terms
    -            * except that the holder is Tim Hudson (tjh@cryptsoft.com).
    -            *
    -            * Copyright remains Eric Young's, and as such any Copyright notices in
    -            * the code are not to be removed.
    -            * If this package is used in a product, Eric Young should be given attribution
    -            * as the author of the parts of the library used.
    -            * This can be in the form of a textual message at program startup or
    -            * in documentation (online or textual) provided with the package.
    -            *
    -            * 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 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. All advertising materials mentioning features or use of this software
    -            *    must display the following acknowledgement:
    -            *    "This product includes cryptographic software written by
    -            *     Eric Young (eay@cryptsoft.com)"
    -            *    The word 'cryptographic' can be left out if the rouines from the library
    -            *    being used are not cryptographic related :-).
    -            * 4. If you include any Windows specific code (or a derivative thereof) from
    -            *    the apps directory (application code) you must include an acknowledgement:
    -            *    "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
    -            *
    -            * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``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 AUTHOR 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.
    -            *
    -            * The licence and distribution terms for any publically available version or
    -            * derivative of this code cannot be changed.  i.e. this code cannot simply be
    -            * copied and put under another distribution licence
    -            * [including the GNU Public Licence.]
    -            */
    -
    -
    -            ISC license used for completely new code in BoringSSL:
    -
    -            /* Copyright (c) 2015, Google Inc.
    -            *
    -            * Permission to use, copy, modify, and/or distribute this software for any
    -            * purpose with or without fee is hereby granted, provided that the above
    -            * copyright notice and this permission notice appear in all copies.
    -            *
    -            * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    -            * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    -            * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
    -            * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    -            * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
    -            * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
    -            * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
    -
    -
    -            Some files from Intel carry the following license:
    -
    -            # Copyright (c) 2012, Intel Corporation
    -            #
    -            # All rights reserved.
    -            #
    -            # Redistribution and use in source and binary forms, with or without
    -            # modification, are permitted provided that the following conditions are
    -            # met:
    -            #
    -            # *  Redistributions of source code must retain the above copyright
    -            #    notice, this list of conditions and the following disclaimer.
    -            #
    -            # *  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.
    -            #
    -            # *  Neither the name of the Intel Corporation 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 INTEL CORPORATION ""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 INTEL CORPORATION 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.
    -
    -        
    -

    jsoncpp

    -
    -            The JsonCpp library's source code, including accompanying documentation,
    -            tests and demonstration applications, are licensed under the following
    -            conditions...
    -
    -            The author (Baptiste Lepilleur) explicitly disclaims copyright in all
    -            jurisdictions which recognize such a disclaimer. In such jurisdictions,
    -            this software is released into the Public Domain.
    -
    -            In jurisdictions which do not recognize Public Domain property (e.g. Germany as of
    -            2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is
    -            released under the terms of the MIT License (see below).
    -
    -            In jurisdictions which recognize Public Domain property, the user of this
    -            software may choose to accept it either as 1) Public Domain, 2) under the
    -            conditions of the MIT License (see below), or 3) under the terms of dual
    -            Public Domain/MIT License conditions described here, as they choose.
    -
    -            The MIT License is about as close to Public Domain as a license can get, and is
    -            described in clear, concise terms at:
    -
    -            http://en.wikipedia.org/wiki/MIT_License
    -
    -            The full text of the MIT License follows:
    -
    -            ========================================================================
    -            Copyright (c) 2007-2010 Baptiste Lepilleur
    -
    -            Permission is hereby granted, free of charge, to any person
    -            obtaining a copy of this software and associated documentation
    -            files (the "Software"), to deal in the Software without
    -            restriction, including without limitation the rights to use, copy,
    -            modify, merge, publish, distribute, sublicense, and/or sell copies
    -            of the Software, and to permit persons to whom the Software is
    -            furnished to do so, subject to the following conditions:
    -
    -            The above copyright notice and this permission notice shall be
    -            included in all copies or substantial portions of the Software.
    -
    -            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    -            EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    -            MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    -            NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
    -            BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
    -            ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    -            CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    -            SOFTWARE.
    -            ========================================================================
    -            (END LICENSE TEXT)
    -
    -            The MIT license is compatible with both the GPL and commercial
    -            software, affording one all of the rights of Public Domain with the
    -            minor nuisance of being required to keep the above copyright notice
    -            and license text in the source code. Note also that by accepting the
    -            Public Domain "license" you can re-license your copy using whatever
    -            license you like.
    -
    -        
    -

    opus

    -
    -            Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic,
    -            Jean-Marc Valin, Timothy B. Terriberry,
    -            CSIRO, Gregory Maxwell, Mark Borgerding,
    -            Erik de Castro Lopo
    -
    -            Redistribution and use in source and binary forms, with or without
    -            modification, are permitted provided that the following conditions
    -            are met:
    -
    -            - Redistributions of source code must retain the above copyright
    -            notice, this list of conditions and the following disclaimer.
    -
    -            - 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.
    -
    -            - Neither the name of Internet Society, IETF or IETF Trust, nor the
    -            names of specific 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 OWNER
    -            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.
    -
    -            Opus is subject to the royalty-free patent licenses which are
    -            specified at:
    -
    -            Xiph.Org Foundation:
    -            https://datatracker.ietf.org/ipr/1524/
    -
    -            Microsoft Corporation:
    -            https://datatracker.ietf.org/ipr/1914/
    -
    -            Broadcom Corporation:
    -            https://datatracker.ietf.org/ipr/1526/
    -
    -        
    -

    protobuf_lite

    -
    -            This license applies to all parts of Protocol Buffers except the following:
    -
    -            - Atomicops support for generic gcc, located in
    -            src/google/protobuf/stubs/atomicops_internals_generic_gcc.h.
    -            This file is copyrighted by Red Hat Inc.
    -
    -            - Atomicops support for AIX/POWER, located in
    -            src/google/protobuf/stubs/atomicops_internals_power.h.
    -            This file is copyrighted by Bloomberg Finance LP.
    -
    -            Copyright 2014, Google Inc.  All rights reserved.
    -
    -            Redistribution and use in source and binary forms, with or without
    -            modification, are permitted provided that the following conditions are
    -            met:
    -
    -            * Redistributions of source code must retain the above copyright
    -            notice, this list of conditions and the following disclaimer.
    -            * 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.
    -            * Neither the name of Google Inc. 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
    -            OWNER 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.
    -
    -            Code generated by the Protocol Buffer compiler is owned by the owner
    -            of the input file used when generating it.  This code is not
    -            standalone and requires a support library to be linked with it.  This
    -            support library is itself covered by the above license.
    -
    -        
    -

    srtp

    -
    -            /*
    -            *
    -            * Copyright (c) 2001-2006 Cisco Systems, Inc.
    -            * All rights reserved.
    -            *
    -            * Redistribution and use in source and binary forms, with or without
    -            * modification, are permitted provided that the following conditions
    -            * are met:
    -            *
    -            *   Redistributions of source code must retain the above copyright
    -            *   notice, this list of conditions and the following disclaimer.
    -            *
    -            *   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.
    -            *
    -            *   Neither the name of the Cisco Systems, Inc. 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 HOLDERS 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.
    -            *
    -            */
    -
    -        
    -

    usrsctplib

    -
    -            (Copied from the COPYRIGHT file of
    -            https://code.google.com/p/sctp-refimpl/source/browse/trunk/COPYRIGHT)
    -            --------------------------------------------------------------------------------
    -
    -            Copyright (c) 2001, 2002 Cisco Systems, Inc.
    -            Copyright (c) 2002-12 Randall R. Stewart
    -            Copyright (c) 2002-12 Michael Tuexen
    -            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.
    -
    -            THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
    -
    -        
    -

    vpx

    -
    -            Copyright (c) 2010, The WebM Project authors. All rights reserved.
    -
    -            Redistribution and use in source and binary forms, with or without
    -            modification, are permitted provided that the following conditions are
    -            met:
    -            
    -            * Redistributions of source code must retain the above copyright
    -            notice, this list of conditions and the following disclaimer.
    -            
    -            * 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.
    -            
    -            * Neither the name of Google, nor the WebM Project, 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.
    -            
    -            
    -        
    -

    yuv

    -
    -            Copyright 2011 The LibYuv Project Authors. All rights reserved.
    -            
    -            Redistribution and use in source and binary forms, with or without
    -            modification, are permitted provided that the following conditions are
    -            met:
    -            
    -            * Redistributions of source code must retain the above copyright
    -            notice, this list of conditions and the following disclaimer.
    -            
    -            * 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.
    -            
    -            * Neither the name of Google 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.
    -        
    -

    + Jitsi Meet iOS SDK (Jitsi Meet iOS SDK binaries) +

    It is composed of 2 frameworks:

    +
      +
    • + JitsiMeet.framework +

      Copyright 2018-present 8x8, Inc. +

      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: +

      https://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. +

      +
    • +
    • + WebRTC.framework +

      webrtc

      +
      +                    Copyright (c) 2011, The WebRTC project authors. All rights reserved.
      +                    
      +                    Redistribution and use in source and binary forms, with or without
      +                    modification, are permitted provided that the following conditions are
      +                    met:
      +                    
      +                    * Redistributions of source code must retain the above copyright
      +                    notice, this list of conditions and the following disclaimer.
      +                    
      +                    * 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.
      +                    
      +                    * Neither the name of Google 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.
      +                    
      +                    This source tree contains third party source code which is governed by third
      +                    party licenses. Paths to the files and associated licenses are collected here.
      +                    
      +                    Files governed by third party licenses:
      +                    common_audio/fft4g.c
      +                    common_audio/signal_processing/spl_sqrt_floor.c
      +                    common_audio/signal_processing/spl_sqrt_floor_arm.S
      +                    modules/audio_coding/codecs/g711/main/source/g711.c
      +                    modules/audio_coding/codecs/g711/main/source/g711.h
      +                    modules/audio_coding/codecs/g722/main/source/g722_decode.c
      +                    modules/audio_coding/codecs/g722/main/source/g722_enc_dec.h
      +                    modules/audio_coding/codecs/g722/main/source/g722_encode.c
      +                    modules/audio_coding/codecs/isac/main/source/fft.c
      +                    modules/audio_device/mac/portaudio/pa_memorybarrier.h
      +                    modules/audio_device/mac/portaudio/pa_ringbuffer.c
      +                    modules/audio_device/mac/portaudio/pa_ringbuffer.h
      +                    modules/audio_processing/aec/aec_rdft.c
      +                    system_wrappers/source/condition_variable_event_win.cc
      +                    system_wrappers/source/set_thread_name_win.h
      +                    system_wrappers/source/spreadsortlib/constants.hpp
      +                    system_wrappers/source/spreadsortlib/spreadsort.hpp
      +                    
      +                    Individual licenses for each file:
      +                    -------------------------------------------------------------------------------
      +                    Files:
      +                    common_audio/signal_processing/spl_sqrt_floor.c
      +                    common_audio/signal_processing/spl_sqrt_floor_arm.S
      +                    
      +                    License:
      +                    /*
      +                    * Written by Wilco Dijkstra, 1996. The following email exchange establishes the
      +                    * license.
      +                    *
      +                    * From: Wilco Dijkstra <Wilco.Dijkstra@ntlworld.com>
      +                    * Date: Fri, Jun 24, 2011 at 3:20 AM
      +                    * Subject: Re: sqrt routine
      +                    * To: Kevin Ma <kma@google.com>
      +                    * Hi Kevin,
      +                    * Thanks for asking. Those routines are public domain (originally posted to
      +                    * comp.sys.arm a long time ago), so you can use them freely for any purpose.
      +                    * Cheers,
      +                    * Wilco
      +                    *
      +                    * ----- Original Message -----
      +                    * From: "Kevin Ma" <kma@google.com>
      +                    * To: <Wilco.Dijkstra@ntlworld.com>
      +                    * Sent: Thursday, June 23, 2011 11:44 PM
      +                    * Subject: Fwd: sqrt routine
      +                    * Hi Wilco,
      +                    * I saw your sqrt routine from several web sites, including
      +                    * http://www.finesse.demon.co.uk/steven/sqrt.html.
      +                    * Just wonder if there's any copyright information with your Successive
      +                    * approximation routines, or if I can freely use it for any purpose.
      +                    * Thanks.
      +                    * Kevin
      +                    */
      +                    -------------------------------------------------------------------------------
      +                    Files:
      +                    modules/audio_coding/codecs/g711/main/source/g711.c
      +                    modules/audio_coding/codecs/g711/main/source/g711.h
      +                    
      +                    License:
      +                    /*
      +                    * SpanDSP - a series of DSP components for telephony
      +                    *
      +                    * g711.h - In line A-law and u-law conversion routines
      +                    *
      +                    * Written by Steve Underwood <steveu@coppice.org>
      +                    *
      +                    * Copyright (C) 2001 Steve Underwood
      +                    *
      +                    *  Despite my general liking of the GPL, I place this code in the
      +                    *  public domain for the benefit of all mankind - even the slimy
      +                    *  ones who might try to proprietize my work and use it to my
      +                    *  detriment.
      +                    */
      +                    -------------------------------------------------------------------------------
      +                    Files:
      +                    modules/audio_coding/codecs/g722/main/source/g722_decode.c
      +                    modules/audio_coding/codecs/g722/main/source/g722_enc_dec.h
      +                    modules/audio_coding/codecs/g722/main/source/g722_encode.c
      +                    
      +                    License:
      +                    /*
      +                    * SpanDSP - a series of DSP components for telephony
      +                    *
      +                    * g722_decode.c - The ITU G.722 codec, decode part.
      +                    *
      +                    * Written by Steve Underwood <steveu@coppice.org>
      +                    *
      +                    * Copyright (C) 2005 Steve Underwood
      +                    *
      +                    *  Despite my general liking of the GPL, I place my own contributions
      +                    *  to this code in the public domain for the benefit of all mankind -
      +                    *  even the slimy ones who might try to proprietize my work and use it
      +                    *  to my detriment.
      +                    *
      +                    * Based in part on a single channel G.722 codec which is:
      +                    *
      +                    * Copyright (c) CMU 1993
      +                    * Computer Science, Speech Group
      +                    * Chengxiang Lu and Alex Hauptmann
      +                    */
      +                    -------------------------------------------------------------------------------
      +                    Files:
      +                    modules/audio_coding/codecs/isac/main/source/fft.c
      +                    
      +                    License:
      +                    /*
      +                    * Copyright(c)1995,97 Mark Olesen <olesen@me.QueensU.CA>
      +                    *    Queen's Univ at Kingston (Canada)
      +                    *
      +                    * Permission to use, copy, modify, and distribute this software for
      +                    * any purpose without fee is hereby granted, provided that this
      +                    * entire notice is included in all copies of any software which is
      +                    * or includes a copy or modification of this software and in all
      +                    * copies of the supporting documentation for such software.
      +                    *
      +                    * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR
      +                    * IMPLIED WARRANTY.  IN PARTICULAR, NEITHER THE AUTHOR NOR QUEEN'S
      +                    * UNIVERSITY AT KINGSTON MAKES ANY REPRESENTATION OR WARRANTY OF ANY
      +                    * KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS
      +                    * FITNESS FOR ANY PARTICULAR PURPOSE.
      +                    *
      +                    * All of which is to say that you can do what you like with this
      +                    * source code provided you don't try to sell it as your own and you
      +                    * include an unaltered copy of this message (including the
      +                    * copyright).
      +                    *
      +                    * It is also implicitly understood that bug fixes and improvements
      +                    * should make their way back to the general Internet community so
      +                    * that everyone benefits.
      +                    */
      +                    -------------------------------------------------------------------------------
      +                    Files:
      +                    modules/audio_device/mac/portaudio/pa_memorybarrier.h
      +                    modules/audio_device/mac/portaudio/pa_ringbuffer.c
      +                    modules/audio_device/mac/portaudio/pa_ringbuffer.h
      +                    
      +                    License:
      +                    /*
      +                    * $Id: pa_memorybarrier.h 1240 2007-07-17 13:05:07Z bjornroche $
      +                    * Portable Audio I/O Library
      +                    * Memory barrier utilities
      +                    *
      +                    * Author: Bjorn Roche, XO Audio, LLC
      +                    *
      +                    * This program uses the PortAudio Portable Audio Library.
      +                    * For more information see: http://www.portaudio.com
      +                    * Copyright (c) 1999-2000 Ross Bencina and Phil Burk
      +                    *
      +                    * Permission is hereby granted, free of charge, to any person obtaining
      +                    * a copy of this software and associated documentation files
      +                    * (the "Software"), to deal in the Software without restriction,
      +                    * including without limitation the rights to use, copy, modify, merge,
      +                    * publish, distribute, sublicense, and/or sell copies of the Software,
      +                    * and to permit persons to whom the Software is furnished to do so,
      +                    * subject to the following conditions:
      +                    *
      +                    * The above copyright notice and this permission notice shall be
      +                    * included in all copies or substantial portions of the Software.
      +                    *
      +                    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
      +                    * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
      +                    * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
      +                    * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
      +                    * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
      +                    * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
      +                    * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      +                    */
      +                    
      +                    /*
      +                    * The text above constitutes the entire PortAudio license; however,
      +                    * the PortAudio community also makes the following non-binding requests:
      +                    *
      +                    * Any person wishing to distribute modifications to the Software is
      +                    * requested to send the modifications to the original developer so that
      +                    * they can be incorporated into the canonical version. It is also
      +                    * requested that these non-binding requests be included along with the
      +                    * license above.
      +                    */
      +                    
      +                    /*
      +                    * $Id: pa_ringbuffer.c 1421 2009-11-18 16:09:05Z bjornroche $
      +                    * Portable Audio I/O Library
      +                    * Ring Buffer utility.
      +                    *
      +                    * Author: Phil Burk, http://www.softsynth.com
      +                    * modified for SMP safety on Mac OS X by Bjorn Roche
      +                    * modified for SMP safety on Linux by Leland Lucius
      +                    * also, allowed for const where possible
      +                    * modified for multiple-byte-sized data elements by Sven Fischer
      +                    *
      +                    * Note that this is safe only for a single-thread reader and a
      +                    * single-thread writer.
      +                    *
      +                    * This program uses the PortAudio Portable Audio Library.
      +                    * For more information see: http://www.portaudio.com
      +                    * Copyright (c) 1999-2000 Ross Bencina and Phil Burk
      +                    *
      +                    * Permission is hereby granted, free of charge, to any person obtaining
      +                    * a copy of this software and associated documentation files
      +                    * (the "Software"), to deal in the Software without restriction,
      +                    * including without limitation the rights to use, copy, modify, merge,
      +                    * publish, distribute, sublicense, and/or sell copies of the Software,
      +                    * and to permit persons to whom the Software is furnished to do so,
      +                    * subject to the following conditions:
      +                    *
      +                    * The above copyright notice and this permission notice shall be
      +                    * included in all copies or substantial portions of the Software.
      +                    *
      +                    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
      +                    * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
      +                    * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
      +                    * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
      +                    * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
      +                    * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
      +                    * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      +                    */
      +                    
      +                    /*
      +                    * The text above constitutes the entire PortAudio license; however,
      +                    * the PortAudio community also makes the following non-binding requests:
      +                    *
      +                    * Any person wishing to distribute modifications to the Software is
      +                    * requested to send the modifications to the original developer so that
      +                    * they can be incorporated into the canonical version. It is also
      +                    * requested that these non-binding requests be included along with the
      +                    * license above.
      +                    */
      +                    -------------------------------------------------------------------------------
      +                    Files:
      +                    common_audio/fft4g.c
      +                    modules/audio_processing/aec/aec_rdft.c
      +                    
      +                    License:
      +                    /*
      +                    * http://www.kurims.kyoto-u.ac.jp/~ooura/fft.html
      +                    * Copyright Takuya OOURA, 1996-2001
      +                    *
      +                    * You may use, copy, modify and distribute this code for any purpose (include
      +                    * commercial use) and without fee. Please refer to this package when you modify
      +                    * this code.
      +                    */
      +                    -------------------------------------------------------------------------------
      +                    Files:
      +                    system_wrappers/source/condition_variable_event_win.cc
      +                    
      +                    Source:
      +                    http://www1.cse.wustl.edu/~schmidt/ACE-copying.html
      +                    
      +                    License:
      +                    Copyright and Licensing Information for ACE(TM), TAO(TM), CIAO(TM), DAnCE(TM),
      +                    and CoSMIC(TM)
      +                    
      +                    ACE(TM), TAO(TM), CIAO(TM), DAnCE>(TM), and CoSMIC(TM) (henceforth referred to
      +                    as "DOC software") are copyrighted by Douglas C. Schmidt and his research
      +                    group at Washington University, University of California, Irvine, and
      +                    Vanderbilt University, Copyright (c) 1993-2009, all rights reserved. Since DOC
      +                    software is open-source, freely available software, you are free to use,
      +                    modify, copy, and distribute--perpetually and irrevocably--the DOC software
      +                    source code and object code produced from the source, as well as copy and
      +                    distribute modified versions of this software. You must, however, include this
      +                    copyright statement along with any code built using DOC software that you
      +                    release. No copyright statement needs to be provided if you just ship binary
      +                    executables of your software products.
      +                    You can use DOC software in commercial and/or binary software releases and are
      +                    under no obligation to redistribute any of your source code that is built
      +                    using DOC software. Note, however, that you may not misappropriate the DOC
      +                    software code, such as copyrighting it yourself or claiming authorship of the
      +                    DOC software code, in a way that will prevent DOC software from being
      +                    distributed freely using an open-source development model. You needn't inform
      +                    anyone that you're using DOC software in your software, though we encourage
      +                    you to let us know so we can promote your project in the DOC software success
      +                    stories.
      +                    
      +                    The ACE, TAO, CIAO, DAnCE, and CoSMIC web sites are maintained by the DOC
      +                    Group at the Institute for Software Integrated Systems (ISIS) and the Center
      +                    for Distributed Object Computing of Washington University, St. Louis for the
      +                    development of open-source software as part of the open-source software
      +                    community. Submissions are provided by the submitter ``as is'' with no
      +                    warranties whatsoever, including any warranty of merchantability,
      +                    noninfringement of third party intellectual property, or fitness for any
      +                    particular purpose. In no event shall the submitter be liable for any direct,
      +                    indirect, special, exemplary, punitive, or consequential damages, including
      +                    without limitation, lost profits, even if advised of the possibility of such
      +                    damages. Likewise, DOC software is provided as is with no warranties of any
      +                    kind, including the warranties of design, merchantability, and fitness for a
      +                    particular purpose, noninfringement, or arising from a course of dealing,
      +                    usage or trade practice. Washington University, UC Irvine, Vanderbilt
      +                    University, their employees, and students shall have no liability with respect
      +                    to the infringement of copyrights, trade secrets or any patents by DOC
      +                    software or any part thereof. Moreover, in no event will Washington
      +                    University, UC Irvine, or Vanderbilt University, their employees, or students
      +                    be liable for any lost revenue or profits or other special, indirect and
      +                    consequential damages.
      +                    
      +                    DOC software is provided with no support and without any obligation on the
      +                    part of Washington University, UC Irvine, Vanderbilt University, their
      +                    employees, or students to assist in its use, correction, modification, or
      +                    enhancement. A number of companies around the world provide commercial support
      +                    for DOC software, however. DOC software is Y2K-compliant, as long as the
      +                    underlying OS platform is Y2K-compliant. Likewise, DOC software is compliant
      +                    with the new US daylight savings rule passed by Congress as "The Energy Policy
      +                    Act of 2005," which established new daylight savings times (DST) rules for the
      +                    United States that expand DST as of March 2007. Since DOC software obtains
      +                    time/date and calendaring information from operating systems users will not be
      +                    affected by the new DST rules as long as they upgrade their operating systems
      +                    accordingly.
      +                    
      +                    The names ACE(TM), TAO(TM), CIAO(TM), DAnCE(TM), CoSMIC(TM), Washington
      +                    University, UC Irvine, and Vanderbilt University, may not be used to endorse
      +                    or promote products or services derived from this source without express
      +                    written permission from Washington University, UC Irvine, or Vanderbilt
      +                    University. This license grants no permission to call products or services
      +                    derived from this source ACE(TM), TAO(TM), CIAO(TM), DAnCE(TM), or CoSMIC(TM),
      +                    nor does it grant permission for the name Washington University, UC Irvine, or
      +                    Vanderbilt University to appear in their names.
      +                    -------------------------------------------------------------------------------
      +                    Files:
      +                    system_wrappers/source/set_thread_name_win.h
      +                    
      +                    Source:
      +                    http://msdn.microsoft.com/en-us/cc300389.aspx#P
      +                    
      +                    License:
      +                    This license governs use of code marked as “sample” or “example” available on
      +                    this web site without a license agreement, as provided under the section above
      +                    titled “NOTICE SPECIFIC TO SOFTWARE AVAILABLE ON THIS WEB SITE.” If you use
      +                    such code (the “software”), you accept this license. If you do not accept the
      +                    license, do not use the software.
      +                    
      +                    1. Definitions
      +                    
      +                    The terms “reproduce,” “reproduction,” “derivative works,” and “distribution”
      +                    have the same meaning here as under U.S. copyright law.
      +                    
      +                    A “contribution” is the original software, or any additions or changes to the
      +                    software.
      +                    
      +                    A “contributor” is any person that distributes its contribution under this
      +                    license.
      +                    
      +                    “Licensed patents” are a contributor’s patent claims that read directly on its
      +                    contribution.
      +                    
      +                    2. Grant of Rights
      +                    
      +                    (A) Copyright Grant - Subject to the terms of this license, including the
      +                    license conditions and limitations in section 3, each contributor grants you a
      +                    non-exclusive, worldwide, royalty-free copyright license to reproduce its
      +                    contribution, prepare derivative works of its contribution, and distribute its
      +                    contribution or any derivative works that you create.
      +                    
      +                    (B) Patent Grant - Subject to the terms of this license, including the license
      +                    conditions and limitations in section 3, each contributor grants you a
      +                    non-exclusive, worldwide, royalty-free license under its licensed patents to
      +                    make, have made, use, sell, offer for sale, import, and/or otherwise dispose
      +                    of its contribution in the software or derivative works of the contribution in
      +                    the software.
      +                    
      +                    3. Conditions and Limitations
      +                    
      +                    (A) No Trademark License- This license does not grant you rights to use any
      +                    contributors’ name, logo, or trademarks.
      +                    
      +                    (B) If you bring a patent claim against any contributor over patents that you
      +                    claim are infringed by the software, your patent license from such contributor
      +                    to the software ends automatically.
      +                    
      +                    (C) If you distribute any portion of the software, you must retain all
      +                    copyright, patent, trademark, and attribution notices that are present in the
      +                    software.
      +                    
      +                    (D) If you distribute any portion of the software in source code form, you may
      +                    do so only under this license by including a complete copy of this license
      +                    with your distribution. If you distribute any portion of the software in
      +                    compiled or object code form, you may only do so under a license that complies
      +                    with this license.
      +                    
      +                    (E) The software is licensed “as-is.” You bear the risk of using it. The
      +                    contributors give no express warranties, guarantees or conditions. You may
      +                    have additional consumer rights under your local laws which this license
      +                    cannot change. To the extent permitted under your local laws, the contributors
      +                    exclude the implied warranties of merchantability, fitness for a particular
      +                    purpose and non-infringement.
      +                    
      +                    (F) Platform Limitation - The licenses granted in sections 2(A) and 2(B)
      +                    extend only to the software or derivative works that you create that run on a
      +                    Microsoft Windows operating system product.
      +                    -------------------------------------------------------------------------------
      +                    Files:
      +                    system_wrappers/source/spreadsortlib/constants.hpp
      +                    system_wrappers/source/spreadsortlib/spreadsort.hpp
      +                    
      +                    License:
      +                    /*Boost Software License - Version 1.0 - August 17th, 2003
      +                    
      +                    Permission is hereby granted, free of charge, to any person or organization
      +                    obtaining a copy of the software and accompanying documentation covered by
      +                    this license (the "Software") to use, reproduce, display, distribute,
      +                    execute, and transmit the Software, and to prepare derivative works of the
      +                    Software, and to permit third-parties to whom the Software is furnished to
      +                    do so, all subject to the following:
      +                    
      +                    The copyright notices in the Software and this entire statement, including
      +                    the above license grant, this restriction and the following disclaimer,
      +                    must be included in all copies of the Software, in whole or in part, and
      +                    all derivative works of the Software, unless such copies or derivative
      +                    works are solely in the form of machine-executable object code generated by
      +                    a source language processor.
      +                    
      +                    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      +                    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      +                    FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
      +                    SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
      +                    FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
      +                    ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
      +                    DEALINGS IN THE SOFTWARE.*/
      +                    
      +                
      +

      boringssl

      +
      +                    BoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL
      +                    licensing. Files that are completely new have a Google copyright and an ISC
      +                    license. This license is reproduced at the bottom of this file.
      +                    
      +                    Contributors to BoringSSL are required to follow the CLA rules for Chromium:
      +                    https://cla.developers.google.com/clas
      +                    
      +                    Some files from Intel are under yet another license, which is also included
      +                    underneath.
      +                    
      +                    The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the
      +                    OpenSSL License and the original SSLeay license apply to the toolkit. See below
      +                    for the actual license texts. Actually both licenses are BSD-style Open Source
      +                    licenses. In case of any license issues related to OpenSSL please contact
      +                    openssl-core@openssl.org.
      +                    
      +                    The following are Google-internal bug numbers where explicit permission from
      +                    some authors is recorded for use of their work. (This is purely for our own
      +                    record keeping.)
      +                    27287199
      +                    27287880
      +                    27287883
      +                    
      +                    OpenSSL License
      +                    ---------------
      +                    
      +                    /* ====================================================================
      +                    * Copyright (c) 1998-2011 The OpenSSL Project.  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. All advertising materials mentioning features or use of this
      +                    *    software must display the following acknowledgment:
      +                    *    "This product includes software developed by the OpenSSL Project
      +                    *    for use in the OpenSSL Toolkit. (http://www.openssl.org/)"
      +                    *
      +                    * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to
      +                    *    endorse or promote products derived from this software without
      +                    *    prior written permission. For written permission, please contact
      +                    *    openssl-core@openssl.org.
      +                    *
      +                    * 5. Products derived from this software may not be called "OpenSSL"
      +                    *    nor may "OpenSSL" appear in their names without prior written
      +                    *    permission of the OpenSSL Project.
      +                    *
      +                    * 6. Redistributions of any form whatsoever must retain the following
      +                    *    acknowledgment:
      +                    *    "This product includes software developed by the OpenSSL Project
      +                    *    for use in the OpenSSL Toolkit (http://www.openssl.org/)"
      +                    *
      +                    * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY
      +                    * EXPRESSED 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 OpenSSL PROJECT OR
      +                    * ITS 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.
      +                    * ====================================================================
      +                    *
      +                    * This product includes cryptographic software written by Eric Young
      +                    * (eay@cryptsoft.com).  This product includes software written by Tim
      +                    * Hudson (tjh@cryptsoft.com).
      +                    *
      +                    */
      +                    
      +                    Original SSLeay License
      +                    -----------------------
      +                    
      +                    /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)
      +                    * All rights reserved.
      +                    *
      +                    * This package is an SSL implementation written
      +                    * by Eric Young (eay@cryptsoft.com).
      +                    * The implementation was written so as to conform with Netscapes SSL.
      +                    *
      +                    * This library is free for commercial and non-commercial use as long as
      +                    * the following conditions are aheared to.  The following conditions
      +                    * apply to all code found in this distribution, be it the RC4, RSA,
      +                    * lhash, DES, etc., code; not just the SSL code.  The SSL documentation
      +                    * included with this distribution is covered by the same copyright terms
      +                    * except that the holder is Tim Hudson (tjh@cryptsoft.com).
      +                    *
      +                    * Copyright remains Eric Young's, and as such any Copyright notices in
      +                    * the code are not to be removed.
      +                    * If this package is used in a product, Eric Young should be given attribution
      +                    * as the author of the parts of the library used.
      +                    * This can be in the form of a textual message at program startup or
      +                    * in documentation (online or textual) provided with the package.
      +                    *
      +                    * 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 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. All advertising materials mentioning features or use of this software
      +                    *    must display the following acknowledgement:
      +                    *    "This product includes cryptographic software written by
      +                    *     Eric Young (eay@cryptsoft.com)"
      +                    *    The word 'cryptographic' can be left out if the rouines from the library
      +                    *    being used are not cryptographic related :-).
      +                    * 4. If you include any Windows specific code (or a derivative thereof) from
      +                    *    the apps directory (application code) you must include an acknowledgement:
      +                    *    "This product includes software written by Tim Hudson (tjh@cryptsoft.com)"
      +                    *
      +                    * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``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 AUTHOR 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.
      +                    *
      +                    * The licence and distribution terms for any publically available version or
      +                    * derivative of this code cannot be changed.  i.e. this code cannot simply be
      +                    * copied and put under another distribution licence
      +                    * [including the GNU Public Licence.]
      +                    */
      +                    
      +                    
      +                    ISC license used for completely new code in BoringSSL:
      +                    
      +                    /* Copyright (c) 2015, Google Inc.
      +                    *
      +                    * Permission to use, copy, modify, and/or distribute this software for any
      +                    * purpose with or without fee is hereby granted, provided that the above
      +                    * copyright notice and this permission notice appear in all copies.
      +                    *
      +                    * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      +                    * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
      +                    * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
      +                    * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
      +                    * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
      +                    * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
      +                    * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
      +                    
      +                    
      +                    Some files from Intel carry the following license:
      +                    
      +                    # Copyright (c) 2012, Intel Corporation
      +                    #
      +                    # All rights reserved.
      +                    #
      +                    # Redistribution and use in source and binary forms, with or without
      +                    # modification, are permitted provided that the following conditions are
      +                    # met:
      +                    #
      +                    # *  Redistributions of source code must retain the above copyright
      +                    #    notice, this list of conditions and the following disclaimer.
      +                    #
      +                    # *  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.
      +                    #
      +                    # *  Neither the name of the Intel Corporation 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 INTEL CORPORATION ""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 INTEL CORPORATION 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.
      +                    
      +                
      +

      jsoncpp

      +
      +                    The JsonCpp library's source code, including accompanying documentation,
      +                    tests and demonstration applications, are licensed under the following
      +                    conditions...
      +                    
      +                    The author (Baptiste Lepilleur) explicitly disclaims copyright in all
      +                    jurisdictions which recognize such a disclaimer. In such jurisdictions,
      +                    this software is released into the Public Domain.
      +                    
      +                    In jurisdictions which do not recognize Public Domain property (e.g. Germany as of
      +                    2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is
      +                    released under the terms of the MIT License (see below).
      +                    
      +                    In jurisdictions which recognize Public Domain property, the user of this
      +                    software may choose to accept it either as 1) Public Domain, 2) under the
      +                    conditions of the MIT License (see below), or 3) under the terms of dual
      +                    Public Domain/MIT License conditions described here, as they choose.
      +                    
      +                    The MIT License is about as close to Public Domain as a license can get, and is
      +                    described in clear, concise terms at:
      +                    
      +                    http://en.wikipedia.org/wiki/MIT_License
      +                    
      +                    The full text of the MIT License follows:
      +                    
      +                    ========================================================================
      +                    Copyright (c) 2007-2010 Baptiste Lepilleur
      +                    
      +                    Permission is hereby granted, free of charge, to any person
      +                    obtaining a copy of this software and associated documentation
      +                    files (the "Software"), to deal in the Software without
      +                    restriction, including without limitation the rights to use, copy,
      +                    modify, merge, publish, distribute, sublicense, and/or sell copies
      +                    of the Software, and to permit persons to whom the Software is
      +                    furnished to do so, subject to the following conditions:
      +                    
      +                    The above copyright notice and this permission notice shall be
      +                    included in all copies or substantial portions of the Software.
      +                    
      +                    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
      +                    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
      +                    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
      +                    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
      +                    BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
      +                    ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
      +                    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
      +                    SOFTWARE.
      +                    ========================================================================
      +                    (END LICENSE TEXT)
      +                    
      +                    The MIT license is compatible with both the GPL and commercial
      +                    software, affording one all of the rights of Public Domain with the
      +                    minor nuisance of being required to keep the above copyright notice
      +                    and license text in the source code. Note also that by accepting the
      +                    Public Domain "license" you can re-license your copy using whatever
      +                    license you like.
      +                    
      +                
      +

      opus

      +
      +                    Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic,
      +                    Jean-Marc Valin, Timothy B. Terriberry,
      +                    CSIRO, Gregory Maxwell, Mark Borgerding,
      +                    Erik de Castro Lopo
      +                    
      +                    Redistribution and use in source and binary forms, with or without
      +                    modification, are permitted provided that the following conditions
      +                    are met:
      +                    
      +                    - Redistributions of source code must retain the above copyright
      +                    notice, this list of conditions and the following disclaimer.
      +                    
      +                    - 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.
      +                    
      +                    - Neither the name of Internet Society, IETF or IETF Trust, nor the
      +                    names of specific 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 OWNER
      +                    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.
      +                    
      +                    Opus is subject to the royalty-free patent licenses which are
      +                    specified at:
      +                    
      +                    Xiph.Org Foundation:
      +                    https://datatracker.ietf.org/ipr/1524/
      +                    
      +                    Microsoft Corporation:
      +                    https://datatracker.ietf.org/ipr/1914/
      +                    
      +                    Broadcom Corporation:
      +                    https://datatracker.ietf.org/ipr/1526/
      +                    
      +                
      +

      protobuf_lite

      +
      +                    This license applies to all parts of Protocol Buffers except the following:
      +                    
      +                    - Atomicops support for generic gcc, located in
      +                    src/google/protobuf/stubs/atomicops_internals_generic_gcc.h.
      +                    This file is copyrighted by Red Hat Inc.
      +                    
      +                    - Atomicops support for AIX/POWER, located in
      +                    src/google/protobuf/stubs/atomicops_internals_power.h.
      +                    This file is copyrighted by Bloomberg Finance LP.
      +                    
      +                    Copyright 2014, Google Inc.  All rights reserved.
      +                    
      +                    Redistribution and use in source and binary forms, with or without
      +                    modification, are permitted provided that the following conditions are
      +                    met:
      +                    
      +                    * Redistributions of source code must retain the above copyright
      +                    notice, this list of conditions and the following disclaimer.
      +                    * 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.
      +                    * Neither the name of Google Inc. 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
      +                    OWNER 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.
      +                    
      +                    Code generated by the Protocol Buffer compiler is owned by the owner
      +                    of the input file used when generating it.  This code is not
      +                    standalone and requires a support library to be linked with it.  This
      +                    support library is itself covered by the above license.
      +                    
      +                
      +

      srtp

      +
      +                    /*
      +                    *
      +                    * Copyright (c) 2001-2006 Cisco Systems, Inc.
      +                    * All rights reserved.
      +                    *
      +                    * Redistribution and use in source and binary forms, with or without
      +                    * modification, are permitted provided that the following conditions
      +                    * are met:
      +                    *
      +                    *   Redistributions of source code must retain the above copyright
      +                    *   notice, this list of conditions and the following disclaimer.
      +                    *
      +                    *   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.
      +                    *
      +                    *   Neither the name of the Cisco Systems, Inc. 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 HOLDERS 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.
      +                    *
      +                    */
      +                    
      +                
      +

      usrsctplib

      +
      +                    (Copied from the COPYRIGHT file of
      +                    https://code.google.com/p/sctp-refimpl/source/browse/trunk/COPYRIGHT)
      +                    --------------------------------------------------------------------------------
      +                    
      +                    Copyright (c) 2001, 2002 Cisco Systems, Inc.
      +                    Copyright (c) 2002-12 Randall R. Stewart
      +                    Copyright (c) 2002-12 Michael Tuexen
      +                    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.
      +                    
      +                    THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
      +                    
      +                
      +

      vpx

      +
      +                    Copyright (c) 2010, The WebM Project authors. All rights reserved.
      +                    
      +                    Redistribution and use in source and binary forms, with or without
      +                    modification, are permitted provided that the following conditions are
      +                    met:
      +                    
      +                    * Redistributions of source code must retain the above copyright
      +                    notice, this list of conditions and the following disclaimer.
      +                    
      +                    * 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.
      +                    
      +                    * Neither the name of Google, nor the WebM Project, 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.
      +                    
      +                    
      +                
      +

      yuv

      +
      +                    Copyright 2011 The LibYuv Project Authors. All rights reserved.
      +                    
      +                    Redistribution and use in source and binary forms, with or without
      +                    modification, are permitted provided that the following conditions are
      +                    met:
      +                    
      +                    * Redistributions of source code must retain the above copyright
      +                    notice, this list of conditions and the following disclaimer.
      +                    
      +                    * 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.
      +                    
      +                    * Neither the name of Google 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.
      +                
      +
    • +
  • Realm ( -#define VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH 39 - #define VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_X 48 #define VECTOR_ROOMBUBBLETABLEVIEWCELL_MARK_WIDTH 4 @@ -36,112 +34,166 @@ @implementation MXKRoomBubbleTableViewCell (Riot) - (void)addTimestampLabelForComponent:(NSUInteger)componentIndex { - self.bubbleInfoContainer.hidden = NO; - MXKRoomBubbleComponent *component; NSArray *bubbleComponents = bubbleData.bubbleComponents; if (componentIndex < bubbleComponents.count) { - component = bubbleComponents[componentIndex]; + component = bubbleComponents[componentIndex]; } if (component && component.date) { - // Check whether this is the first displayed component. BOOL isFirstDisplayedComponent = (componentIndex == 0); + BOOL isLastMessageMostRecentComponent = NO; + + RoomBubbleCellData *roomBubbleCellData; + if ([bubbleData isKindOfClass:RoomBubbleCellData.class]) { - isFirstDisplayedComponent = (componentIndex == ((RoomBubbleCellData*)bubbleData).oldestComponentIndex); + roomBubbleCellData = (RoomBubbleCellData*)bubbleData; + isFirstDisplayedComponent = (componentIndex == roomBubbleCellData.oldestComponentIndex); + isLastMessageMostRecentComponent = roomBubbleCellData.containsLastMessage && (componentIndex == roomBubbleCellData.mostRecentComponentIndex); } - CGFloat timeLabelPosX = self.bubbleInfoContainer.frame.size.width - VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH; - CGFloat timeLabelPosY = isFirstDisplayedComponent ? 0 : component.position.y + self.msgTextViewTopConstraint.constant - self.bubbleInfoContainerTopConstraint.constant; - UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(timeLabelPosX, timeLabelPosY, VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH , 18)]; + // Display timestamp on the left for selected component when it cannot overlap other UI elements like user's avatar + BOOL displayLabelOnLeft = roomBubbleCellData.displayTimestampForSelectedComponentOnLeftWhenPossible + && !isLastMessageMostRecentComponent + && ( !isFirstDisplayedComponent || roomBubbleCellData.shouldHideSenderInformation); - timeLabel.text = [bubbleData.eventFormatter timeStringFromDate:component.date]; - timeLabel.textAlignment = NSTextAlignmentRight; - timeLabel.textColor = ThemeService.shared.theme.textSecondaryColor; - if ([UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) + [self addTimestampLabelForComponentIndex:componentIndex + isFirstDisplayedComponent:isFirstDisplayedComponent + viewTag:componentIndex + displayOnLeft:displayLabelOnLeft]; + } +} + +- (void)addTimestampLabelForComponentIndex:(NSInteger)componentIndex + isFirstDisplayedComponent:(BOOL)isFirstDisplayedComponent + viewTag:(NSInteger)viewTag + displayOnLeft:(BOOL)displayOnLeft +{ + NSArray *bubbleComponents = bubbleData.bubbleComponents; + MXKRoomBubbleComponent *component = bubbleComponents[componentIndex]; + + self.bubbleInfoContainer.hidden = NO; + + CGFloat timeLabelPosX; + CGFloat timeLabelPosY; + CGFloat timeLabelHeight = RoomBubbleCellLayout.timestampLabelHeight; + CGFloat timeLabelWidth; + NSTextAlignment timeLabelTextAlignment; + + CGRect componentFrame = [self componentFrameInContentViewForIndex:componentIndex]; + + if (displayOnLeft) + { + CGFloat leftMargin = 10.0; + CGFloat rightMargin = (self.contentView.frame.size.width - (self.bubbleInfoContainer.frame.origin.x + self.bubbleInfoContainer.frame.size.width)); + + timeLabelPosX = 0; + + if (CGRectEqualToRect(componentFrame, CGRectNull) == false) { - timeLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight]; + timeLabelPosY = componentFrame.origin.y - self.bubbleInfoContainerTopConstraint.constant; } else { - timeLabel.font = [UIFont systemFontOfSize:12]; + timeLabelPosY = component.position.y + self.msgTextViewTopConstraint.constant - self.bubbleInfoContainerTopConstraint.constant; } - timeLabel.adjustsFontSizeToFitWidth = YES; - - timeLabel.tag = componentIndex; - - [timeLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; - timeLabel.accessibilityIdentifier = @"timestampLabel"; - [self.bubbleInfoContainer addSubview:timeLabel]; - - // Define timeLabel constraints (to handle auto-layout in case of screen rotation) - NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:timeLabel - attribute:NSLayoutAttributeTrailing - relatedBy:NSLayoutRelationEqual - toItem:self.bubbleInfoContainer - attribute:NSLayoutAttributeTrailing - multiplier:1.0 - constant:0]; - NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:timeLabel - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:self.bubbleInfoContainer - attribute:NSLayoutAttributeTop - multiplier:1.0 - constant:timeLabelPosY]; - NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:timeLabel - attribute:NSLayoutAttributeWidth - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1.0 - constant:VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH]; - NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:timeLabel - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1.0 - constant:18]; - // Available on iOS 8 and later - [NSLayoutConstraint activateConstraints:@[rightConstraint, topConstraint, widthConstraint, heightConstraint]]; + timeLabelWidth = self.contentView.frame.size.width - leftMargin - rightMargin; + timeLabelTextAlignment = NSTextAlignmentLeft; + } + else + { + timeLabelPosX = self.bubbleInfoContainer.frame.size.width - RoomBubbleCellLayout.timestampLabelWidth; - // Check whether a vertical whitespace was applied to display correctly the timestamp. - if (!isFirstDisplayedComponent || bubbleData.shouldHideSenderInformation || bubbleData.shouldHideSenderName) + if (isFirstDisplayedComponent) { - // Adjust the position of the potential encryption icon in this case. - if (self.encryptionStatusContainerView) - { - NSArray* subviews = self.encryptionStatusContainerView.subviews; - for (UIView *view in subviews) - { - // Note: The encryption icon has been tagged with the component index. - if (view.tag == componentIndex) - { - CGRect frame = view.frame; - frame.origin.y += 15; - view.frame = frame; - - break; - } - } - } + timeLabelPosY = 0; + } + else if (CGRectEqualToRect(componentFrame, CGRectNull) == false) + { + timeLabelPosY = componentFrame.origin.y - self.bubbleInfoContainerTopConstraint.constant - timeLabelHeight; } + else + { + timeLabelPosY = component.position.y + self.msgTextViewTopConstraint.constant - timeLabelHeight - self.bubbleInfoContainerTopConstraint.constant; + } + + timeLabelWidth = RoomBubbleCellLayout.timestampLabelWidth; + timeLabelTextAlignment = NSTextAlignmentRight; } + + timeLabelPosY = MAX(0.0, timeLabelPosY); + + UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(timeLabelPosX, timeLabelPosY, timeLabelWidth, timeLabelHeight)]; + + timeLabel.text = [bubbleData.eventFormatter timeStringFromDate:component.date]; + timeLabel.textAlignment = timeLabelTextAlignment; + timeLabel.textColor = ThemeService.shared.theme.textSecondaryColor; + timeLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightLight]; + timeLabel.adjustsFontSizeToFitWidth = YES; + + timeLabel.tag = viewTag; + + [timeLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; + timeLabel.accessibilityIdentifier = @"timestampLabel"; + [self.bubbleInfoContainer addSubview:timeLabel]; + + // Define timeLabel constraints (to handle auto-layout in case of screen rotation) + NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:timeLabel + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.bubbleInfoContainer + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:0]; + NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:timeLabel + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.bubbleInfoContainer + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:timeLabelPosY]; + + NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:timeLabel + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:timeLabelWidth]; + + + NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:timeLabel + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:timeLabelHeight]; + + // Available on iOS 8 and later + [NSLayoutConstraint activateConstraints:@[rightConstraint, topConstraint, widthConstraint, heightConstraint]]; } - (void)selectComponent:(NSUInteger)componentIndex +{ + [self selectComponent:componentIndex showEditButton:NO showTimestamp:YES]; +} + +- (void)selectComponent:(NSUInteger)componentIndex showEditButton:(BOOL)showEditButton showTimestamp:(BOOL)showTimestamp { if (componentIndex < bubbleData.bubbleComponents.count) { - // Add time label - [self addTimestampLabelForComponent:componentIndex]; + if (showTimestamp) + { + // Add time label + [self addTimestampLabelForComponent:componentIndex]; + } // Blur timestamp labels which are not related to the selected component (if any) for (UIView* view in self.bubbleInfoContainer.subviews) @@ -164,8 +216,11 @@ - (void)selectComponent:(NSUInteger)componentIndex } } - // Add the edit button - [self addEditButtonForComponent:componentIndex completion:nil]; + if (showEditButton) + { + // Add the edit button + [self addEditButtonForComponent:componentIndex completion:nil]; + } } } @@ -260,7 +315,7 @@ - (void)addDateLabel NSDate *date = bubbleData.date; if (date) { - UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.bubbleInfoContainer.frame.size.width , 18)]; + UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.bubbleInfoContainer.frame.size.width, RoomBubbleCellLayout.timestampLabelHeight)]; timeLabel.text = [bubbleData.eventFormatter dateStringFromDate:date withTime:NO]; timeLabel.textAlignment = NSTextAlignmentRight; @@ -307,7 +362,7 @@ - (void)addDateLabel toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 - constant:18]; + constant:RoomBubbleCellLayout.timestampLabelHeight]; // Available on iOS 8 and later [NSLayoutConstraint activateConstraints:@[rightConstraint, topConstraint, widthConstraint, heightConstraint]]; @@ -412,6 +467,187 @@ - (void)updateUserNameColor } } +- (CGRect)componentFrameInTableViewForIndex:(NSInteger)componentIndex +{ + CGRect componentFrameInContentView = [self componentFrameInContentViewForIndex:componentIndex]; + return [self.contentView convertRect:componentFrameInContentView toView:self.superview]; +} + +- (CGRect)surroundingFrameInTableViewForComponentIndex:(NSInteger)componentIndex +{ + CGRect surroundingFrame; + + CGRect componentFrameInContentView = [self componentFrameInContentViewForIndex:componentIndex]; + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = self; + MXKRoomBubbleCellData *bubbleCellData = roomBubbleTableViewCell.bubbleData; + + NSInteger firstVisibleComponentIndex = NSNotFound; + NSInteger lastMostRecentComponentIndex = NSNotFound; + + if ([bubbleCellData isKindOfClass:[RoomBubbleCellData class]]) + { + RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleCellData; + firstVisibleComponentIndex = [roomBubbleCellData firstVisibleComponentIndex]; + + if (roomBubbleCellData.containsLastMessage + && roomBubbleCellData.mostRecentComponentIndex != NSNotFound + && roomBubbleCellData.firstVisibleComponentIndex != roomBubbleCellData.mostRecentComponentIndex + && componentIndex == roomBubbleCellData.mostRecentComponentIndex) + { + lastMostRecentComponentIndex = roomBubbleCellData.mostRecentComponentIndex; + } + } + + // Do not overlap timestamp for last message + if (lastMostRecentComponentIndex != NSNotFound) + { + CGFloat componentBottomY = componentFrameInContentView.origin.y + componentFrameInContentView.size.height; + + CGFloat x = 0; + CGFloat y = componentFrameInContentView.origin.y - RoomBubbleCellLayout.timestampLabelHeight; + CGFloat width = roomBubbleTableViewCell.contentView.frame.size.width; + CGFloat height = componentBottomY - y; + + surroundingFrame = CGRectMake(x, y, width, height); + } // Do not overlap user name label for first visible component + else if (!CGRectEqualToRect(componentFrameInContentView, CGRectNull) + && firstVisibleComponentIndex != NSNotFound + && componentIndex <= firstVisibleComponentIndex + && roomBubbleTableViewCell.userNameLabel + && roomBubbleTableViewCell.userNameLabel.isHidden == NO) + { + CGFloat componentBottomY = componentFrameInContentView.origin.y + componentFrameInContentView.size.height; + + CGFloat x = 0; + CGFloat y = roomBubbleTableViewCell.userNameLabel.frame.origin.y; + CGFloat width = roomBubbleTableViewCell.contentView.frame.size.width; + CGFloat height = componentBottomY - y; + + surroundingFrame = CGRectMake(x, y, width, height); + } + else + { + surroundingFrame = componentFrameInContentView; + } + + return [self.contentView convertRect:surroundingFrame toView:self.superview]; +} + +- (CGRect)componentFrameInContentViewForIndex:(NSInteger)componentIndex +{ + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = self; + MXKRoomBubbleCellData *bubbleCellData = roomBubbleTableViewCell.bubbleData; + MXKRoomBubbleComponent *selectedComponent; + + if (bubbleCellData.bubbleComponents.count > componentIndex) + { + selectedComponent = bubbleCellData.bubbleComponents[componentIndex]; + } + + if (!selectedComponent) + { + return CGRectNull; + } + + CGFloat selectedComponenContentViewYOffset = 0; + CGFloat selectedComponentPositionY = 0; + CGFloat selectedComponentHeight = 0; + + CGRect componentFrame = CGRectNull; + + if (roomBubbleTableViewCell.attachmentView) + { + CGRect attachamentViewFrame = roomBubbleTableViewCell.attachmentView.frame; + + selectedComponenContentViewYOffset = attachamentViewFrame.origin.y; + selectedComponentHeight = attachamentViewFrame.size.height; + } + else if (roomBubbleTableViewCell.messageTextView) + { + CGFloat textMessageHeight = 0; + + if ([bubbleCellData isKindOfClass:[RoomBubbleCellData class]]) + { + RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleCellData; + + if (!roomBubbleCellData.attachment && selectedComponent.attributedTextMessage) + { + textMessageHeight = [roomBubbleCellData rawTextHeight:selectedComponent.attributedTextMessage]; + } + } + + selectedComponentPositionY = selectedComponent.position.y; + + if (textMessageHeight > 0) + { + selectedComponentHeight = textMessageHeight; + } + else + { + selectedComponentHeight = roomBubbleTableViewCell.frame.size.height - selectedComponentPositionY; + } + + selectedComponenContentViewYOffset = roomBubbleTableViewCell.messageTextView.frame.origin.y; + } + + if (roomBubbleTableViewCell.attachmentView || roomBubbleTableViewCell.messageTextView) + { + CGFloat x = 0; + CGFloat y = selectedComponenContentViewYOffset + selectedComponentPositionY; + CGFloat width = roomBubbleTableViewCell.contentView.frame.size.width; + + componentFrame = CGRectMake(x, y, width, selectedComponentHeight); + } + else + { + componentFrame = roomBubbleTableViewCell.bounds; + } + + return componentFrame; +} + ++ (CGFloat)attachmentBubbleCellHeightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + MXKRoomBubbleTableViewCell* cell = [self cellWithOriginalXib]; + CGFloat rowHeight = 0; + + RoomBubbleCellData *bubbleData; + + if ([cellData isKindOfClass:[RoomBubbleCellData class]]) + { + bubbleData = (RoomBubbleCellData*)cellData; + } + + if (bubbleData && cell.attachmentView && bubbleData.isAttachmentWithThumbnail) + { + // retrieve the suggested image view height + rowHeight = bubbleData.contentSize.height; + + // Check here the minimum height defined in cell view for text message + if (cell.attachViewMinHeightConstraint && rowHeight < cell.attachViewMinHeightConstraint.constant) + { + rowHeight = cell.attachViewMinHeightConstraint.constant; + } + + // Finalize the row height by adding the vertical constraints. + + rowHeight += cell.attachViewTopConstraint.constant; + + CGFloat additionalHeight = bubbleData.additionalContentHeight; + + if (additionalHeight) + { + rowHeight += additionalHeight; + } + else + { + rowHeight += cell.attachViewBottomConstraint.constant; + } + } + + return rowHeight; +} + #pragma mark - User actions - (IBAction)onEditButtonPressed:(id)sender @@ -460,7 +696,7 @@ - (void)addEditButtonForComponent:(NSUInteger)componentIndex completion:(void (^ // Define 'Edit' button frame UIImage *editIcon = [UIImage imageNamed:@"edit_icon"]; - CGFloat editBtnPosX = self.bubbleInfoContainer.frame.size.width - VECTOR_ROOMBUBBLETABLEVIEWCELL_TIMELABEL_WIDTH - 22 - editIcon.size.width / 2; + CGFloat editBtnPosX = self.bubbleInfoContainer.frame.size.width - RoomBubbleCellLayout.timestampLabelWidth - 22 - editIcon.size.width / 2; CGFloat editBtnPosY = isFirstDisplayedComponent ? -13 : component.position.y + self.msgTextViewTopConstraint.constant - self.bubbleInfoContainerTopConstraint.constant - 13; UIButton *editButton = [[UIButton alloc] initWithFrame:CGRectMake(editBtnPosX, editBtnPosY, 44, 44)]; diff --git a/Riot/Categories/UIGestureRecognizer.swift b/Riot/Categories/UIGestureRecognizer.swift new file mode 100644 index 0000000000..26dd05082f --- /dev/null +++ b/Riot/Categories/UIGestureRecognizer.swift @@ -0,0 +1,28 @@ +/* + 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 UIGestureRecognizer { + + func vc_isTouchingInside(view: UIView? = nil) -> Bool { + guard let view = view ?? self.view else { + return false + } + let touchedLocation = self.location(in: view) + return view.bounds.contains(touchedLocation) + } +} diff --git a/Riot/Categories/UIImage.swift b/Riot/Categories/UIImage.swift new file mode 100644 index 0000000000..9c6847645d --- /dev/null +++ b/Riot/Categories/UIImage.swift @@ -0,0 +1,51 @@ +/* + 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 UIImage { + + class func vc_image(from color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage? { + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + UIGraphicsBeginImageContext(rect.size) + let context = UIGraphicsGetCurrentContext() + + context?.setFillColor(color.cgColor) + context?.fill(rect) + + var image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + UIGraphicsBeginImageContext(size) + image?.draw(in: rect) + image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return image + } + + func vc_tintedImage(usingColor tintColor: UIColor) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) + let drawRect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height) + + self.draw(in: drawRect) + tintColor.set() + UIRectFillUsingBlendMode(drawRect, .sourceAtop) + let tintedImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return tintedImage + } +} diff --git a/Riot/Categories/UIStackView.swift b/Riot/Categories/UIStackView.swift new file mode 100644 index 0000000000..e1ebb800b0 --- /dev/null +++ b/Riot/Categories/UIStackView.swift @@ -0,0 +1,28 @@ +/* + 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 UIStackView { + + func vc_removeAllSubviews() { + let subviews = self.arrangedSubviews + for subview in subviews { + self.removeArrangedSubview(subview) + subview.removeFromSuperview() + } + } +} diff --git a/Riot/Categories/UITouch.swift b/Riot/Categories/UITouch.swift new file mode 100644 index 0000000000..be7e8307e8 --- /dev/null +++ b/Riot/Categories/UITouch.swift @@ -0,0 +1,28 @@ +/* + 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 UITouch { + + func vc_isInside(view: UIView? = nil) -> Bool { + guard let view = view ?? self.view else { + return false + } + let touchedLocation = self.location(in: view) + return view.bounds.contains(touchedLocation) + } +} diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 486035dc0c..5007a2609f 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -75,6 +75,10 @@ internal enum Asset { internal static let scrolldown = ImageAsset(name: "scrolldown") internal static let scrollup = ImageAsset(name: "scrollup") internal static let typing = ImageAsset(name: "typing") + internal static let roomContextMenuCopy = ImageAsset(name: "room_context_menu_copy") + internal static let roomContextMenuEdit = ImageAsset(name: "room_context_menu_edit") + internal static let roomContextMenuMore = ImageAsset(name: "room_context_menu_more") + internal static let roomContextMenuReply = ImageAsset(name: "room_context_menu_reply") internal static let uploadIcon = ImageAsset(name: "upload_icon") internal static let voiceCallIcon = ImageAsset(name: "voice_call_icon") internal static let addParticipant = ImageAsset(name: "add_participant") diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift index 5db46ea188..b8647542a8 100644 --- a/Riot/Generated/Storyboards.swift +++ b/Riot/Generated/Storyboards.swift @@ -37,6 +37,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: DeviceVerificationVerifyViewController.self) } + internal enum EditHistoryViewController: StoryboardType { + internal static let storyboardName = "EditHistoryViewController" + + internal static let initialScene = InitialSceneType(storyboard: EditHistoryViewController.self) + } internal enum KeyBackupRecoverFromPassphraseViewController: StoryboardType { internal static let storyboardName = "KeyBackupRecoverFromPassphraseViewController" @@ -72,6 +77,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: KeyBackupSetupSuccessFromRecoveryKeyViewController.self) } + internal enum RoomContextualMenuViewController: StoryboardType { + internal static let storyboardName = "RoomContextualMenuViewController" + + internal static let initialScene = InitialSceneType(storyboard: RoomContextualMenuViewController.self) + } internal enum SimpleScreenTemplateViewController: StoryboardType { internal static let storyboardName = "SimpleScreenTemplateViewController" diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 87287b6659..0738f36855 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -334,6 +334,10 @@ internal enum VectorL10n { internal static var cancel: String { return VectorL10n.tr("Vector", "cancel") } + /// Close + internal static var close: String { + return VectorL10n.tr("Vector", "close") + } /// collapse internal static var collapse: String { return VectorL10n.tr("Vector", "collapse") @@ -482,7 +486,7 @@ internal enum VectorL10n { internal static var deviceVerificationEmojiCat: String { return VectorL10n.tr("Vector", "device_verification_emoji_cat") } - /// Class + /// Clock internal static var deviceVerificationEmojiClock: String { return VectorL10n.tr("Vector", "device_verification_emoji_clock") } @@ -574,6 +578,10 @@ internal enum VectorL10n { internal static var deviceVerificationEmojiLion: String { return VectorL10n.tr("Vector", "device_verification_emoji_lion") } + /// Lock + internal static var deviceVerificationEmojiLock: String { + return VectorL10n.tr("Vector", "device_verification_emoji_lock") + } /// Moon internal static var deviceVerificationEmojiMoon: String { return VectorL10n.tr("Vector", "device_verification_emoji_moon") @@ -586,10 +594,6 @@ internal enum VectorL10n { internal static var deviceVerificationEmojiOctopus: String { return VectorL10n.tr("Vector", "device_verification_emoji_octopus") } - /// Padlock - internal static var deviceVerificationEmojiPadlock: String { - return VectorL10n.tr("Vector", "device_verification_emoji_padlock") - } /// Panda internal static var deviceVerificationEmojiPanda: String { return VectorL10n.tr("Vector", "device_verification_emoji_panda") @@ -886,6 +890,10 @@ internal enum VectorL10n { internal static func eventFormatterMemberUpdates(_ p1: Int) -> String { return VectorL10n.tr("Vector", "event_formatter_member_updates", p1) } + /// (edited) + internal static var eventFormatterMessageEditedMention: String { + return VectorL10n.tr("Vector", "event_formatter_message_edited_mention") + } /// Re-request encryption keys internal static var eventFormatterRerequestKeysPart1Link: String { return VectorL10n.tr("Vector", "event_formatter_rerequest_keys_part1_link") @@ -902,6 +910,14 @@ internal enum VectorL10n { internal static func eventFormatterWidgetRemoved(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "event_formatter_widget_removed", p1, p2) } + /// File upload + internal static var fileUploadErrorTitle: String { + return VectorL10n.tr("Vector", "file_upload_error_title") + } + /// File type not supported. + internal static var fileUploadErrorUnsupportedFileTypeMessage: String { + return VectorL10n.tr("Vector", "file_upload_error_unsupported_file_type_message") + } /// To continue using the %@ homeserver you must review and agree to the terms and conditions. internal static func gdprConsentNotGivenAlertMessage(_ p1: String) -> String { return VectorL10n.tr("Vector", "gdpr_consent_not_given_alert_message", p1) @@ -1338,6 +1354,14 @@ internal enum VectorL10n { internal static var retry: String { return VectorL10n.tr("Vector", "retry") } + /// Reply + internal static var roomActionReply: String { + return VectorL10n.tr("Vector", "room_action_reply") + } + /// Send file + internal static var roomActionSendFile: String { + return VectorL10n.tr("Vector", "room_action_send_file") + } /// Send photo or video internal static var roomActionSendPhotoOrVideo: String { return VectorL10n.tr("Vector", "room_action_send_photo_or_video") @@ -1694,6 +1718,10 @@ internal enum VectorL10n { internal static var roomEventActionDelete: String { return VectorL10n.tr("Vector", "room_event_action_delete") } + /// Edit + internal static var roomEventActionEdit: String { + return VectorL10n.tr("Vector", "room_event_action_edit") + } /// Reason for kicking this user internal static var roomEventActionKickPromptReason: String { return VectorL10n.tr("Vector", "room_event_action_kick_prompt_reason") @@ -1710,10 +1738,22 @@ internal enum VectorL10n { internal static var roomEventActionQuote: String { return VectorL10n.tr("Vector", "room_event_action_quote") } + /// Show all + internal static var roomEventActionReactionShowAll: String { + return VectorL10n.tr("Vector", "room_event_action_reaction_show_all") + } + /// Show less + internal static var roomEventActionReactionShowLess: String { + return VectorL10n.tr("Vector", "room_event_action_reaction_show_less") + } /// Remove internal static var roomEventActionRedact: String { return VectorL10n.tr("Vector", "room_event_action_redact") } + /// Reply + internal static var roomEventActionReply: String { + return VectorL10n.tr("Vector", "room_event_action_reply") + } /// Report content internal static var roomEventActionReport: String { return VectorL10n.tr("Vector", "room_event_action_report") @@ -1762,6 +1802,10 @@ internal enum VectorL10n { internal static func roomManyUsersAreTyping(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "room_many_users_are_typing", p1, p2) } + /// Message edits + internal static var roomMessageEditsHistoryTitle: String { + return VectorL10n.tr("Vector", "room_message_edits_history_title") + } /// Send a message (unencrypted)… internal static var roomMessagePlaceholder: String { return VectorL10n.tr("Vector", "room_message_placeholder") @@ -2446,6 +2490,10 @@ internal enum VectorL10n { internal static var settingsLabsE2eEncryptionPromptMessage: String { return VectorL10n.tr("Vector", "settings_labs_e2e_encryption_prompt_message") } + /// React to messages with emoji + internal static var settingsLabsMessageReaction: String { + return VectorL10n.tr("Vector", "settings_labs_message_reaction") + } /// Lazy load rooms members internal static var settingsLabsRoomMembersLazyLoading: String { return VectorL10n.tr("Vector", "settings_labs_room_members_lazy_loading") @@ -2798,6 +2846,14 @@ internal enum VectorL10n { internal static var widgetIntegrationUnableToCreate: String { return VectorL10n.tr("Vector", "widget_integration_unable_to_create") } + /// Failed to connect to integrations server + internal static var widgetIntegrationsServerFailedToConnect: String { + return VectorL10n.tr("Vector", "widget_integrations_server_failed_to_connect") + } + /// No integrations server configured + internal static var widgetNoIntegrationsServerConfigured: String { + return VectorL10n.tr("Vector", "widget_no_integrations_server_configured") + } /// You need permission to manage widgets in this room internal static var widgetNoPowerToManage: String { return VectorL10n.tr("Vector", "widget_no_power_to_manage") diff --git a/Riot/Managers/Analytics/Analytics.m b/Riot/Managers/Analytics/Analytics.m index 552e9e0aba..4491c4158a 100644 --- a/Riot/Managers/Analytics/Analytics.m +++ b/Riot/Managers/Analytics/Analytics.m @@ -31,7 +31,30 @@ NSString *const kAnalyticsE2eDecryptionFailureAction = @"Decryption failure"; -@import PiwikTracker; +@import MatomoTracker; + +@interface MatomoTracker (MatomoTrackerMigration) ++ (MatomoTracker *)shared; + +- (void)migrateFromFourPointFourSharedInstance; +@end + +@implementation MatomoTracker (MatomoTrackerMigration) ++ (MatomoTracker *)shared +{ + NSDictionary *piwikConfig = [[NSUserDefaults standardUserDefaults] objectForKey:@"piwik"]; + MatomoTracker *matomoTracker = [[MatomoTracker alloc] initWithSiteId:piwikConfig[@"siteId"] baseURL:[NSURL URLWithString:piwikConfig[@"url"]] userAgent:@"iOSMatomoTracker"]; + [matomoTracker migrateFromFourPointFourSharedInstance]; + return matomoTracker; +} + +- (void)migrateFromFourPointFourSharedInstance +{ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"migratedFromFourPointFourSharedInstance"]) return; + [self copyFromOldSharedInstance]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"migratedFromFourPointFourSharedInstance"]; +} +@end @implementation Analytics @@ -49,29 +72,24 @@ + (instancetype)sharedInstance - (void)start { - NSDictionary *piwikConfig = [[NSUserDefaults standardUserDefaults] objectForKey:@"piwik"]; - [PiwikTracker configureSharedInstanceWithSiteID:piwikConfig[@"siteId"] - baseURL:[NSURL URLWithString:piwikConfig[@"url"]] - userAgent:@"iOSPiwikTracker"]; - // Check whether the user has enabled the sending of crash reports. if (RiotSettings.shared.enableCrashReport) { - [PiwikTracker shared].isOptedOut = NO; + [MatomoTracker shared].isOptedOut = NO; - [[PiwikTracker shared] setCustomVariableWithIndex:1 name:@"App Platform" value:@"iOS Platform"]; - [[PiwikTracker shared] setCustomVariableWithIndex:2 name:@"App Version" value:[AppDelegate theDelegate].appVersion]; + [[MatomoTracker shared] setCustomVariableWithIndex:1 name:@"App Platform" value:@"iOS Platform"]; + [[MatomoTracker shared] setCustomVariableWithIndex:2 name:@"App Version" value:[AppDelegate theDelegate].appVersion]; // The language is either the one selected by the user within the app // or, else, the one configured by the OS NSString *language = [NSBundle mxk_language] ? [NSBundle mxk_language] : [[NSBundle mainBundle] preferredLocalizations][0]; - [[PiwikTracker shared] setCustomVariableWithIndex:4 name:@"Chosen Language" value:language]; + [[MatomoTracker shared] setCustomVariableWithIndex:4 name:@"Chosen Language" value:language]; MXKAccount* account = [MXKAccountManager sharedManager].activeAccounts.firstObject; if (account) { - [[PiwikTracker shared] setCustomVariableWithIndex:7 name:@"Homeserver URL" value:account.mxCredentials.homeServer]; - [[PiwikTracker shared] setCustomVariableWithIndex:8 name:@"Identity Server URL" value:account.identityServerURL]; + [[MatomoTracker shared] setCustomVariableWithIndex:7 name:@"Homeserver URL" value:account.mxCredentials.homeServer]; + [[MatomoTracker shared] setCustomVariableWithIndex:8 name:@"Identity Server URL" value:account.identityServerURL]; } // TODO: We should also track device and os version @@ -83,20 +101,20 @@ - (void)start #ifdef DEBUG // Disable analytics in debug as it pollutes stats - [PiwikTracker shared].isOptedOut = YES; + [MatomoTracker shared].isOptedOut = YES; #endif } else { NSLog(@"[AppDelegate] The user decided to not send analytics"); - [PiwikTracker shared].isOptedOut = YES; + [MatomoTracker shared].isOptedOut = YES; [MXLogger logCrashes:NO]; } } - (void)stop { - [PiwikTracker shared].isOptedOut = YES; + [MatomoTracker shared].isOptedOut = YES; [MXLogger logCrashes:NO]; } @@ -106,20 +124,20 @@ - (void)trackScreen:(NSString *)screenName NSString *appName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; NSString *appVersion = [AppDelegate theDelegate].appVersion; - [[PiwikTracker shared] trackWithView:@[@"ios", appName, appVersion, screenName] + [[MatomoTracker shared] trackWithView:@[@"ios", appName, appVersion, screenName] url:nil]; } - (void)dispatch { - [[PiwikTracker shared] dispatch]; + [[MatomoTracker shared] dispatch]; } - (void)trackLaunchScreenDisplayDuration:(NSTimeInterval)seconds { NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStartupCategory]; - [[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory + [[MatomoTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory action:action name:kMXAnalyticsStartupLaunchScreen number:@(seconds * 1000) @@ -132,7 +150,7 @@ - (void)trackStartupStorePreloadDuration: (NSTimeInterval)seconds { NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStartupCategory]; - [[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory + [[MatomoTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory action:action name:kMXAnalyticsStartupStorePreload number:@(seconds * 1000) @@ -143,7 +161,7 @@ - (void)trackStartupMountDataDuration: (NSTimeInterval)seconds { NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStartupCategory]; - [[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory + [[MatomoTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory action:action name:kMXAnalyticsStartupMountData number:@(seconds * 1000) @@ -154,7 +172,7 @@ - (void)trackStartupSyncDuration: (NSTimeInterval)seconds isInitial: (BOOL)isIni { NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStartupCategory]; - [[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory + [[MatomoTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory action:action name:isInitial ? kMXAnalyticsStartupInititialSync : kMXAnalyticsStartupIncrementalSync number:@(seconds * 1000) @@ -165,7 +183,7 @@ - (void)trackRoomCount: (NSUInteger)roomCount { NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStatsCategory]; - [[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory + [[MatomoTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory action:action name:kMXAnalyticsStatsRooms number:@(roomCount) @@ -178,7 +196,7 @@ - (void)trackFailures:(NSDictionary *)failuresCounts { for (NSString *reason in failuresCounts) { - [[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsE2eCategory + [[MatomoTracker shared] trackWithEventWithCategory:kAnalyticsE2eCategory action:kAnalyticsE2eDecryptionFailureAction name:reason number:failuresCounts[reason] diff --git a/Riot/Managers/Theme/Themable.swift b/Riot/Managers/Theme/Themable.swift index 3764b24b7f..f4f59f1e9c 100644 --- a/Riot/Managers/Theme/Themable.swift +++ b/Riot/Managers/Theme/Themable.swift @@ -16,6 +16,6 @@ import Foundation -protocol Themable: class { +@objc protocol Themable: class { func update(theme: Theme) } diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 375dd3913d..c8a10674aa 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -38,6 +38,7 @@ import UIKit var textSecondaryColor: UIColor { get } var tintColor: UIColor { get } + var tintBackgroundColor: UIColor { get } var unreadRoomIndentColor: UIColor { get } diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index 1b3651a6e7..c98b27a245 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -30,8 +30,8 @@ class DarkTheme: NSObject, Theme { var searchBackgroundColor: UIColor = UIColor(rgb: 0x181B21) var searchPlaceholderColor: UIColor = UIColor(rgb: 0x61708B) - var headerBackgroundColor: UIColor = UIColor(rgb: 0x15171B) - var headerBorderColor: UIColor = UIColor(rgb: 0x22262E) + var headerBackgroundColor: UIColor = UIColor(rgb: 0x22262E) + var headerBorderColor: UIColor = UIColor(rgb: 0x181B21) var headerTextPrimaryColor: UIColor = UIColor(rgb: 0xA1B2D1) var headerTextSecondaryColor: UIColor = UIColor(rgb: 0xC8C8CD) @@ -39,6 +39,7 @@ class DarkTheme: NSObject, Theme { var textSecondaryColor: UIColor = UIColor(rgb: 0xA1B2D1) var tintColor: UIColor = UIColor(rgb: 0x03B381) + var tintBackgroundColor: UIColor = UIColor(rgb: 0x1F6954) var unreadRoomIndentColor: UIColor = UIColor(rgb: 0x2E3648) var lineBreakColor: UIColor = UIColor(rgb: 0x61708B) diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index d1bcaf9f80..7f154f232e 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -30,7 +30,7 @@ class DefaultTheme: NSObject, Theme { var searchBackgroundColor: UIColor = UIColor(rgb: 0xFFFFFF) var searchPlaceholderColor: UIColor = UIColor(rgb: 0x61708B) - var headerBackgroundColor: UIColor = UIColor(rgb: 0xF2F5F8) + var headerBackgroundColor: UIColor = UIColor(rgb: 0xF3F8FD) var headerBorderColor: UIColor = UIColor(rgb: 0xE9EDF1) var headerTextPrimaryColor: UIColor = UIColor(rgb: 0x61708B) var headerTextSecondaryColor: UIColor = UIColor(rgb: 0xC8C8CD) @@ -39,6 +39,7 @@ class DefaultTheme: NSObject, Theme { var textSecondaryColor: UIColor = UIColor(rgb: 0x9E9E9E) var tintColor: UIColor = UIColor(rgb: 0x03B381) + var tintBackgroundColor: UIColor = UIColor(rgb: 0xe9fff9) var unreadRoomIndentColor: UIColor = UIColor(rgb: 0x2E3648) var lineBreakColor: UIColor = UIColor(rgb: 0xEEEFEF) diff --git a/Riot/Managers/Widgets/Widget.m b/Riot/Managers/Widgets/Widget.m index 482f54e9c9..1438701b3b 100644 --- a/Riot/Managers/Widgets/Widget.m +++ b/Riot/Managers/Widgets/Widget.m @@ -1,5 +1,6 @@ /* 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. @@ -100,7 +101,7 @@ - (MXHTTPOperation *)widgetUrl:(void (^)(NSString * _Nonnull))success failure:(v _widgetId]]; // Check if their scalar token must added - if ([WidgetManager isScalarUrl:widgetUrl]) + if ([[WidgetManager sharedManager] isScalarUrl:widgetUrl forUser:userId]) { return [[WidgetManager sharedManager] getScalarTokenForMXSession:_mxSession validate:NO success:^(NSString *scalarToken) { // Add the user scalar token diff --git a/Riot/Managers/Widgets/WidgetManager.h b/Riot/Managers/Widgets/WidgetManager.h index 83b41ac2fd..a159287891 100644 --- a/Riot/Managers/Widgets/WidgetManager.h +++ b/Riot/Managers/Widgets/WidgetManager.h @@ -1,5 +1,6 @@ /* 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. @@ -20,6 +21,8 @@ #import "Widget.h" +@class WidgetManagerConfig; + /** The type of matrix event used for matrix widgets. */ @@ -51,7 +54,9 @@ FOUNDATION_EXPORT NSString *const WidgetManagerErrorDomain; typedef enum : NSUInteger { WidgetManagerErrorCodeNotEnoughPower, - WidgetManagerErrorCodeCreationFailed + WidgetManagerErrorCodeCreationFailed, + WidgetManagerErrorCodeNoIntegrationsServerConfigured, + WidgetManagerErrorCodeFailedToConnectToIntegrationsServer } WidgetManagerErrorCode; @@ -180,6 +185,30 @@ WidgetManagerErrorCode; #pragma mark - Modular interface +/** + Get the integration manager configuration for a user. + + @param userId the user id. + @return the integration manager configuration. + */ +- (WidgetManagerConfig*)configForUser:(NSString*)userId; + +/** + Store the integration manager configuration for a user. + + @param the integration manager configuration. + @param userId the user id. + */ +- (void)setConfig:(WidgetManagerConfig*)config forUser:(NSString*)userId; + +/** + Check if the user has URLs for an integration manager configured. + + @param userId the user id. + @return YES if they have URLs for an integration manager. + */ +- (BOOL)hasIntegrationManagerForUser:(NSString*)userId; + /** Make sure there is a scalar token for the given Matrix session. @@ -201,8 +230,9 @@ WidgetManagerErrorCode; Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api @param urlString the URL to check. + @param userId the user id. @return YES if specified URL is a scalar URL. */ -+ (BOOL)isScalarUrl:(NSString*)urlString; +- (BOOL)isScalarUrl:(NSString*)urlString forUser:(NSString*)userId; @end diff --git a/Riot/Managers/Widgets/WidgetManager.m b/Riot/Managers/Widgets/WidgetManager.m index 8f761bf080..a6d2bdec16 100644 --- a/Riot/Managers/Widgets/WidgetManager.m +++ b/Riot/Managers/Widgets/WidgetManager.m @@ -1,5 +1,6 @@ /* 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. @@ -16,6 +17,8 @@ #import "WidgetManager.h" +#import "Riot-Swift.h" + #import #pragma mark - Contants @@ -46,7 +49,7 @@ @interface WidgetManager () NSMutableDictionary*> *failureBlockForWidgetCreation; // User id -> scalar token - NSMutableDictionary *scalarTokens; + NSMutableDictionary *configs; } @end @@ -74,12 +77,7 @@ - (instancetype)init successBlockForWidgetCreation = [NSMutableDictionary dictionary]; failureBlockForWidgetCreation = [NSMutableDictionary dictionary]; - [self load]; - - if (!scalarTokens) - { - scalarTokens = [NSMutableDictionary dictionary]; - } + [self loadConfigs]; } return self; } @@ -261,6 +259,15 @@ - (MXHTTPOperation *)createJitsiWidgetInRoom:(MXRoom*)room success:(void (^)(Widget *jitsiWidget))success failure:(void (^)(NSError *error))failure { + NSString *userId = room.mxSession.myUser.userId; + WidgetManagerConfig *config = [self configForUser:userId]; + if (!config.hasUrls) + { + NSLog(@"[WidgetManager] createJitsiWidgetInRoom: Error: no Integrations Manager API URL for user %@", userId); + failure(self.errorForNonConfiguredIntegrationManager); + return nil; + } + // Build data for a jitsi widget NSString *widgetId = [NSString stringWithFormat:@"%@_%@_%@", kWidgetTypeJitsi, room.mxSession.myUser.userId, @((uint64_t)([[NSDate date] timeIntervalSince1970] * 1000))]; @@ -274,8 +281,7 @@ - (MXHTTPOperation *)createJitsiWidgetInRoom:(MXRoom*)room // TODO: This url should come from modular API // Note: this url can be used as is inside a web container (like iframe for Riot-web) // Riot-iOS does not directly use it but extracts params from it (see `[JitsiViewController openWidget:withVideo:]`) - NSString *modularRestUrl = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsRestUrl"]; - NSString *url = [NSString stringWithFormat:@"%@/widgets/jitsi.html?confId=%@&isAudioConf=%@&displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&email=$matrix_user_id@", modularRestUrl, confId, video ? @"false" : @"true"]; + NSString *url = [NSString stringWithFormat:@"%@/widgets/jitsi.html?confId=%@&isAudioConf=%@&displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&email=$matrix_user_id@", config.apiUrl, confId, video ? @"false" : @"true"]; return [self createWidget:widgetId withContent:@{ @@ -435,12 +441,30 @@ - (void)removeMatrixSession:(MXSession *)mxSession - (void)deleteDataForUser:(NSString *)userId { - [scalarTokens removeObjectForKey:userId]; - [self save]; + [configs removeObjectForKey:userId]; + [self saveConfigs]; } #pragma mark - Modular interface +- (WidgetManagerConfig*)configForUser:(NSString*)userId +{ + // Return a default config by default + return configs[userId] ? configs[userId] : [WidgetManagerConfig new]; +} + +- (BOOL)hasIntegrationManagerForUser:(NSString*)userId +{ + return [self configForUser:userId].hasUrls; +} + +- (void)setConfig:(WidgetManagerConfig*)config forUser:(NSString*)userId +{ + configs[userId] = config; + [self saveConfigs]; +} + + - (MXHTTPOperation *)getScalarTokenForMXSession:(MXSession*)mxSession validate:(BOOL)validate success:(void (^)(NSString *scalarToken))success @@ -487,15 +511,22 @@ - (MXHTTPOperation *)registerForScalarToken:(MXSession*)mxSession failure:(void (^)(NSError *error))failure { MXHTTPOperation *operation; + NSString *userId = mxSession.myUser.userId; + + WidgetManagerConfig *config = [self configForUser:userId]; + if (!config.hasUrls) + { + NSLog(@"[WidgetManager] registerForScalarToken: Error: no Integrations Manager API URL for user %@", mxSession.myUser.userId); + failure(self.errorForNonConfiguredIntegrationManager); + return nil; + } MXWeakify(self); operation = [mxSession.matrixRestClient openIdToken:^(MXOpenIdToken *tokenObject) { MXStrongifyAndReturnIfNil(self); // Exchange the token for a scalar token - NSString *modularRestUrl = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsRestUrl"]; - - MXHTTPClient *httpClient = [[MXHTTPClient alloc] initWithBaseURL:modularRestUrl andOnUnrecognizedCertificateBlock:nil]; + MXHTTPClient *httpClient = [[MXHTTPClient alloc] initWithBaseURL:config.apiUrl andOnUnrecognizedCertificateBlock:nil]; MXHTTPOperation *operation2 = [httpClient requestWithMethod:@"POST" @@ -506,9 +537,10 @@ - (MXHTTPOperation *)registerForScalarToken:(MXSession*)mxSession NSString *scalarToken; MXJSONModelSetString(scalarToken, JSONResponse[@"scalar_token"]) - self->scalarTokens[mxSession.myUser.userId] = scalarToken; + config.scalarToken = scalarToken; - [self save]; + self->configs[userId] = config; + [self saveConfigs]; if (success) { @@ -516,10 +548,17 @@ - (MXHTTPOperation *)registerForScalarToken:(MXSession*)mxSession } } failure:^(NSError *error) { - NSLog(@"[WidgetManager] registerForScalarToken. Error in modular/register request"); + NSLog(@"[WidgetManager] registerForScalarToken: Failed to register. Error: %@", error); if (failure) { + // Specialise the error + NSError *error = [NSError errorWithDomain:WidgetManagerErrorDomain + code:WidgetManagerErrorCodeFailedToConnectToIntegrationsServer + userInfo:@{ + NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"widget_integrations_server_failed_to_connect", @"Vector", nil) + }]; + failure(error); } }]; @@ -542,8 +581,17 @@ - (MXHTTPOperation *)validateScalarToken:(NSString*)scalarToken forMXSession:(MX complete:(void (^)(BOOL valid))complete failure:(void (^)(NSError *error))failure { - NSString *modularRestUrl = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsRestUrl"]; - MXHTTPClient *httpClient = [[MXHTTPClient alloc] initWithBaseURL:modularRestUrl andOnUnrecognizedCertificateBlock:nil]; + NSString *userId = mxSession.myUser.userId; + + WidgetManagerConfig *config = [self configForUser:userId]; + if (!config.hasUrls) + { + NSLog(@"[WidgetManager] validateScalarToken: Error: no Integrations Manager API URL for user %@", mxSession.myUser.userId); + failure(self.errorForNonConfiguredIntegrationManager); + return nil; + } + + MXHTTPClient *httpClient = [[MXHTTPClient alloc] initWithBaseURL:config.apiUrl andOnUnrecognizedCertificateBlock:nil]; return [httpClient requestWithMethod:@"GET" path:[NSString stringWithFormat:@"account?v=1.1&scalar_token=%@", scalarToken] @@ -579,14 +627,19 @@ - (MXHTTPOperation *)validateScalarToken:(NSString*)scalarToken forMXSession:(MX }]; } -+ (BOOL)isScalarUrl:(NSString *)urlString +- (BOOL)isScalarUrl:(NSString *)urlString forUser:(NSString*)userId { BOOL isScalarUrl = NO; + // TODO: Do we need to add `integrationsWidgetsUrls` to `WidgetManagerConfig`? NSArray *scalarUrlStrings = [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsWidgetsUrls"]; if (scalarUrlStrings.count == 0) { - scalarUrlStrings = @[[[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsRestUrl"]]; + NSString *apiUrl = [self configForUser:userId].apiUrl; + if (apiUrl) + { + scalarUrlStrings = @[apiUrl]; + } } for (NSString *scalarUrlString in scalarUrlStrings) @@ -605,20 +658,63 @@ + (BOOL)isScalarUrl:(NSString *)urlString - (NSString *)scalarTokenForMXSession:(MXSession *)mxSession { - return scalarTokens[mxSession.myUser.userId]; + return configs[mxSession.myUser.userId].scalarToken; } -- (void)load +- (void)loadConfigs { NSUserDefaults *userDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults; - scalarTokens = [NSMutableDictionary dictionaryWithDictionary:[userDefaults objectForKey:@"scalarTokens"]]; + + NSDictionary *scalarTokens = [userDefaults objectForKey:@"scalarTokens"]; + if (scalarTokens) + { + // Manage migration to WidgetManagerConfig + configs = [NSMutableDictionary dictionary]; + for (NSString *userId in scalarTokens) + { + NSString *scalarToken = scalarTokens[userId]; + + NSLog(@"[WidgetManager] migrate scalarTokens to integrationManagerConfigs for %@", userId); + + WidgetManagerConfig *config = [WidgetManagerConfig new]; + config.scalarToken = scalarToken; + + configs[userId] = config; + } + + [self saveConfigs]; + [userDefaults removeObjectForKey:@"scalarTokens"]; + } + else + { + NSData *configsData = [userDefaults objectForKey:@"integrationManagerConfigs"]; + if (configsData) + { + configs = [NSMutableDictionary dictionaryWithDictionary:[NSKeyedUnarchiver unarchiveObjectWithData:configsData]]; + } + + if (!configs) + { + configs = [NSMutableDictionary dictionary]; + } + } } -- (void)save +- (void)saveConfigs { NSUserDefaults *userDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults; + [userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:configs] + forKey:@"integrationManagerConfigs"]; +} - [userDefaults setObject:scalarTokens forKey:@"scalarTokens"]; + +#pragma mark - Errors + +- (NSError*)errorForNonConfiguredIntegrationManager +{ + return [NSError errorWithDomain:WidgetManagerErrorDomain + code:WidgetManagerErrorCodeNoIntegrationsServerConfigured + userInfo:@{NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"widget_no_integrations_server_configured", @"Vector", nil)}]; } @end diff --git a/Riot/Managers/Widgets/WidgetManagerConfig.swift b/Riot/Managers/Widgets/WidgetManagerConfig.swift new file mode 100644 index 0000000000..ba6c3ac36a --- /dev/null +++ b/Riot/Managers/Widgets/WidgetManagerConfig.swift @@ -0,0 +1,77 @@ +/* + 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 + +/// Configuration for an integration manager. +/// By default, it uses URLs defined in the app settings but they can be overidden. +@objcMembers +class WidgetManagerConfig: NSObject, NSCoding { + + /// The URL for the REST api + let apiUrl: NSString? + /// The URL of the integration manager interface + let uiUrl: NSString? + /// The token if the user has been authenticated + var scalarToken: NSString? + + var hasUrls: Bool { + if apiUrl != nil && uiUrl != nil { + return true + } else { + return false + } + } + + init(apiUrl: NSString?, uiUrl: NSString?) { + self.apiUrl = apiUrl + self.uiUrl = uiUrl + + super.init() + } + + override convenience init () { + // Use app settings as default + let apiUrl = UserDefaults.standard.object(forKey: "integrationsRestUrl") as? NSString + let uiUrl = UserDefaults.standard.object(forKey: "integrationsUiUrl") as? NSString + + self.init(apiUrl: apiUrl, uiUrl: uiUrl) + } + + + /// MARK: - NSCoding + + enum CodingKeys: String { + case apiUrl = "apiUrl" + case uiUrl = "uiUrl" + case scalarToken = "scalarToken" + } + + func encode(with aCoder: NSCoder) { + aCoder.encode(self.apiUrl, forKey: CodingKeys.apiUrl.rawValue) + aCoder.encode(self.uiUrl, forKey: CodingKeys.uiUrl.rawValue) + aCoder.encode(self.scalarToken, forKey: CodingKeys.scalarToken.rawValue) + } + + convenience required init?(coder aDecoder: NSCoder) { + let apiUrl = aDecoder.decodeObject(forKey: CodingKeys.apiUrl.rawValue) as? NSString + let uiUrl = aDecoder.decodeObject(forKey: CodingKeys.uiUrl.rawValue) as? NSString + let scalarToken = aDecoder.decodeObject(forKey: CodingKeys.scalarToken.rawValue) as? NSString + + self.init(apiUrl: apiUrl, uiUrl: uiUrl) + self.scalarToken = scalarToken + } +} diff --git a/Riot/Model/Room/RoomPreviewData.h b/Riot/Model/Room/RoomPreviewData.h index 333912c8f2..47797327c2 100644 --- a/Riot/Model/Room/RoomPreviewData.h +++ b/Riot/Model/Room/RoomPreviewData.h @@ -50,6 +50,12 @@ */ @property (nonatomic) NSString *eventId; +/** + In case of preview, the server names to try and join through in addition to those + that are automatically chosen. + */ +@property (nonatomic) NSArray *viaServers; + /** Preview information. */ diff --git a/Riot/Modules/Authentication/Views/AuthInputsView.m b/Riot/Modules/Authentication/Views/AuthInputsView.m index 79280d25de..5685a9f433 100644 --- a/Riot/Modules/Authentication/Views/AuthInputsView.m +++ b/Riot/Modules/Authentication/Views/AuthInputsView.m @@ -814,8 +814,24 @@ - (void)updateAuthSessionWithCompletedStages:(NSArray *)completedStages didUpdat else if ([self isFlowSupported:kMXLoginFlowTypeTerms] && ![self isFlowCompleted:kMXLoginFlowTypeTerms]) { NSLog(@"[AuthInputsView] Prepare a new terms stage"); - - [self prepareParameters:callback]; + + if (externalRegistrationParameters) + { + [self displayTermsView:^{ + + NSDictionary *parameters = @{ + @"auth": @{ + @"session":self->currentSession.session, + @"type": kMXLoginFlowTypeTerms + } + }; + callback(parameters, nil); + }]; + } + else + { + [self prepareParameters:callback]; + } return; } diff --git a/Riot/Modules/Common/CollectionView/AutosizedCollectionView.swift b/Riot/Modules/Common/CollectionView/AutosizedCollectionView.swift new file mode 100644 index 0000000000..10cdbf75c7 --- /dev/null +++ b/Riot/Modules/Common/CollectionView/AutosizedCollectionView.swift @@ -0,0 +1,31 @@ +/* + 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 + +/// AutosizedCollectionView is a convenient UICollectionView that makes dynamic sizing easier when using Auto Layout +class AutosizedCollectionView: UICollectionView { + + override var contentSize: CGSize { + didSet { + self.invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + return self.contentSize + } +} diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 03e582aeb8..30a754803d 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1815,8 +1815,9 @@ - (void)joinARoom self->currentAlert = nil; [self.activityIndicator startAnimating]; - - self->currentRequest = [self.mainSession joinRoom:roomAliasOrId success:^(MXRoom *room) { + + // TODO + self->currentRequest = [self.mainSession joinRoom:roomAliasOrId viaServers:nil success:^(MXRoom *room) { self->currentRequest = nil; [self.activityIndicator stopAnimating]; diff --git a/Riot/Modules/DeviceVerification/Incoming/DeviceVerificationIncomingViewController.swift b/Riot/Modules/DeviceVerification/Incoming/DeviceVerificationIncomingViewController.swift index 7013886dbe..99874dc1e3 100644 --- a/Riot/Modules/DeviceVerification/Incoming/DeviceVerificationIncomingViewController.swift +++ b/Riot/Modules/DeviceVerification/Incoming/DeviceVerificationIncomingViewController.swift @@ -75,22 +75,6 @@ final class DeviceVerificationIncomingViewController: UIViewController { self.viewModel.viewDelegate = self } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } override var preferredStatusBarStyle: UIStatusBarStyle { return self.theme.statusBarStyle @@ -110,7 +94,9 @@ final class DeviceVerificationIncomingViewController: UIViewController { self.titleLabel.textColor = theme.textPrimaryColor self.description1Label.textColor = theme.textPrimaryColor self.description2Label.textColor = theme.textPrimaryColor - + self.userDisplaynameLabel.textColor = theme.textPrimaryColor + self.deviceIdLabel.textColor = theme.textPrimaryColor + self.continueButtonBackgroundView.backgroundColor = theme.backgroundColor theme.applyStyle(onButton: self.continueButton) } diff --git a/Riot/Modules/DeviceVerification/Start/DeviceVerificationStartViewController.swift b/Riot/Modules/DeviceVerification/Start/DeviceVerificationStartViewController.swift index 9b6d394510..958965cc6a 100644 --- a/Riot/Modules/DeviceVerification/Start/DeviceVerificationStartViewController.swift +++ b/Riot/Modules/DeviceVerification/Start/DeviceVerificationStartViewController.swift @@ -71,22 +71,6 @@ final class DeviceVerificationStartViewController: UIViewController { self.viewModel.viewDelegate = self } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } override var preferredStatusBarStyle: UIStatusBarStyle { return self.theme.statusBarStyle diff --git a/Riot/Modules/DeviceVerification/Verify/DeviceVerificationVerifyViewController.swift b/Riot/Modules/DeviceVerification/Verify/DeviceVerificationVerifyViewController.swift index 80e7136bff..61b4e2fd42 100644 --- a/Riot/Modules/DeviceVerification/Verify/DeviceVerificationVerifyViewController.swift +++ b/Riot/Modules/DeviceVerification/Verify/DeviceVerificationVerifyViewController.swift @@ -78,18 +78,6 @@ final class DeviceVerificationVerifyViewController: UIViewController { // Hide back button self.navigationItem.setHidesBackButton(true, animated: animated) } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } override var preferredStatusBarStyle: UIStatusBarStyle { return self.theme.statusBarStyle diff --git a/Riot/Modules/Integrations/IntegrationManagerViewController.m b/Riot/Modules/Integrations/IntegrationManagerViewController.m index 37bab65fa7..ff9df08fe0 100644 --- a/Riot/Modules/Integrations/IntegrationManagerViewController.m +++ b/Riot/Modules/Integrations/IntegrationManagerViewController.m @@ -1,5 +1,6 @@ /* 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. @@ -18,6 +19,9 @@ #import "WidgetManager.h" +#import "AppDelegate.h" +#import "Riot-Swift.h" + NSString *const kIntegrationManagerMainScreen = nil; NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; @@ -85,8 +89,14 @@ - (void)viewWillAppear:(BOOL)animated } failure:^(NSError *error) { MXStrongifyAndReturnIfNil(self); + NSLog(@"[IntegraionManagerVS] Cannot open due to missing scalar token. Error: %@", error); + self->operation = nil; [self stopActivityIndicator]; + + [self withdrawViewControllerAnimated:YES completion:^{ + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; }]; } } @@ -100,10 +110,12 @@ - (NSString *)interfaceUrl { NSMutableString *url; + NSString *integrationsUiUrl = [[WidgetManager sharedManager] configForUser:mxSession.myUser.userId].uiUrl; + if (scalarToken) { url = [NSMutableString stringWithFormat:@"%@?scalar_token=%@&room_id=%@", - [[NSUserDefaults standardUserDefaults] objectForKey:@"integrationsUiUrl"], + integrationsUiUrl, [MXTools encodeURIComponent:scalarToken], [MXTools encodeURIComponent:roomId] ]; diff --git a/Riot/Modules/Integrations/Widgets/WidgetViewController.m b/Riot/Modules/Integrations/Widgets/WidgetViewController.m index f67456292f..f7ad5c0bdd 100644 --- a/Riot/Modules/Integrations/Widgets/WidgetViewController.m +++ b/Riot/Modules/Integrations/Widgets/WidgetViewController.m @@ -190,7 +190,7 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNaviga NSLog(@"[WidgetVC] decidePolicyForNavigationResponse: statusCode: %@", @(response.statusCode)); } - if (response.statusCode == 403 && [WidgetManager isScalarUrl:self.URL]) + if (response.statusCode == 403 && [[WidgetManager sharedManager] isScalarUrl:self.URL forUser:self.widget.mxSession.myUser.userId]) { [self fixScalarToken]; } diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift new file mode 100644 index 0000000000..5fbe5de8a5 --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift @@ -0,0 +1,68 @@ +/* + 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 BubbleReactionActionViewCell: UICollectionViewCell, NibReusable, Themable { + + // MARK: - Constants + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var actionLabel: UILabel! + + // MARK: Private + + private var theme: Theme? + + // MARK: Public + + // MARK: - Life cycle + + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + if #available(iOS 12.0, *) { + /* + On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : + "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). + (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. + If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." + */ + self.updateConstraintsIfNeeded() + } + return super.preferredLayoutAttributesFitting(layoutAttributes) + } + + // MARK: - Public + + func fill(actionString: String) { + self.actionLabel.text = actionString + self.updateViews() + } + + func update(theme: Theme) { + self.theme = theme + self.updateViews() + } + + // MARK: - Private + + private func updateViews() { + self.actionLabel.textColor = self.theme?.tintColor + } +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.xib b/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.xib new file mode 100644 index 0000000000..fa3e5bdb75 --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.xib @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift new file mode 100644 index 0000000000..c6e2af6269 --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift @@ -0,0 +1,108 @@ +/* + 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 BubbleReactionViewCell: UICollectionViewCell, NibReusable, Themable { + + // MARK: - Constants + + private enum Constants { + static let selectedBorderWidth: CGFloat = 1.0 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var reactionBackgroundView: UIView! + @IBOutlet private weak var emojiLabel: UILabel! + @IBOutlet private weak var countLabel: UILabel! + + // MARK: Private + + private var theme: Theme? + + // MARK: Public + + private var isReactionSelected: Bool = false + + // 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.size.height/2.0 + } + + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + if #available(iOS 12.0, *) { + /* + On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : + "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). + (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. + If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." + */ + self.updateConstraintsIfNeeded() + } + return super.preferredLayoutAttributesFitting(layoutAttributes) + } + + // MARK: - Public + + func fill(viewData: BubbleReactionViewData) { + self.emojiLabel.text = viewData.emoji + self.countLabel.text = viewData.countString + self.isReactionSelected = viewData.isCurrentUserReacted + + self.updateViews() + } + + func update(theme: Theme) { + self.theme = theme + self.reactionBackgroundView.layer.borderColor = theme.tintColor.cgColor + self.emojiLabel.textColor = theme.textPrimaryColor + self.countLabel.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 + } +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.xib b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.xib new file mode 100644 index 0000000000..17552af695 --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.xib @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewData.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewData.swift new file mode 100644 index 0000000000..316f9c34bd --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewData.swift @@ -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 BubbleReactionViewData { + let emoji: String + let countString: String + let isCurrentUserReacted: Bool +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift new file mode 100644 index 0000000000..4ac8923e4e --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.swift @@ -0,0 +1,185 @@ +/* + 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 MatrixSDK +import Reusable +import DGCollectionViewLeftAlignFlowLayout + +@objcMembers +final class BubbleReactionsView: UIView, NibOwnerLoadable { + + // MARK: - Constants + + private enum Constants { + static let minimumInteritemSpacing: CGFloat = 6.0 + static let minimumLineSpacing: CGFloat = 2.0 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var collectionView: UICollectionView! + + // MARK: Private + + private var reactionsViewData: [BubbleReactionViewData] = [] + private var showAllButtonState: BubbleReactionsViewState.ShowAllButtonState = .none + private var theme: Theme? + + // MARK: Public + + // Do not use `BubbleReactionsViewModelType` here due to Objective-C incompatibily + var viewModel: BubbleReactionsViewModel? { + didSet { + self.viewModel?.viewDelegate = self + self.viewModel?.process(viewAction: .loadData) + } + } + + // 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() + } + + convenience init() { + self.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.loadNibContent() + self.commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.loadNibContent() + self.commonInit() + } + + // MARK: - Public + + func update(theme: Theme) { + self.theme = theme + self.collectionView.reloadData() + } + + // MARK: - Private + + private func fill(reactionsViewData: [BubbleReactionViewData], showAllButtonState: BubbleReactionsViewState.ShowAllButtonState) { + self.reactionsViewData = reactionsViewData + self.showAllButtonState = showAllButtonState + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + } + + private func actionButtonString() -> String { + let actionString: String + switch self.showAllButtonState { + case .showAll: + actionString = VectorL10n.roomEventActionReactionShowAll + case .showLess: + actionString = VectorL10n.roomEventActionReactionShowLess + case .none: + actionString = "" + } + + return actionString + } +} + +// MARK: - UICollectionViewDataSource +extension BubbleReactionsView: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + // "Show all" or "Show less" is a cell in the same section as reactions cells + let additionalItems = self.showAllButtonState == .none ? 0 : 1 + + return self.reactionsViewData.count + additionalItems + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + if indexPath.row < self.reactionsViewData.count { + let cell: BubbleReactionViewCell = collectionView.dequeueReusableCell(for: indexPath) + + if let theme = self.theme { + cell.update(theme: theme) + } + + let viewData = self.reactionsViewData[indexPath.row] + cell.fill(viewData: viewData) + + return cell + } else { + let cell: BubbleReactionActionViewCell = collectionView.dequeueReusableCell(for: indexPath) + + if let theme = self.theme { + cell.update(theme: theme) + } + + let actionString = self.actionButtonString() + cell.fill(actionString: actionString) + + return cell + } + } +} + +// MARK: - UICollectionViewDelegate +extension BubbleReactionsView: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if indexPath.row < self.reactionsViewData.count { + self.viewModel?.process(viewAction: .tapReaction(index: indexPath.row)) + } else { + switch self.showAllButtonState { + case .showAll: + self.viewModel?.process(viewAction: .tapShowAction(action: .showAll)) + case .showLess: + self.viewModel?.process(viewAction: .tapShowAction(action: .showLess)) + case .none: + break + } + } + } +} + +// MARK: - BubbleReactionsViewModelViewDelegate +extension BubbleReactionsView: BubbleReactionsViewModelViewDelegate { + + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didUpdateViewState viewState: BubbleReactionsViewState) { + switch viewState { + case .loaded(reactionsViewData: let reactionsViewData, showAllButtonState: let showAllButtonState): + self.fill(reactionsViewData: reactionsViewData, showAllButtonState: showAllButtonState) + } + } +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.xib b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.xib new file mode 100644 index 0000000000..7c5cedb2ad --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionsView.xib @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModel.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModel.swift new file mode 100644 index 0000000000..1a79b6edbe --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModel.swift @@ -0,0 +1,95 @@ +/* + 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 final class BubbleReactionsViewModel: NSObject, BubbleReactionsViewModelType { + + // MARK: - Constants + + private enum Constants { + static let maxItemsWhenLimited: Int = 8 + } + + // MARK: - Properties + + // MARK: Private + + private let aggregatedReactions: MXAggregatedReactions + private let eventId: String + private let showAll: Bool + + // MARK: Public + + @objc weak var viewModelDelegate: BubbleReactionsViewModelDelegate? + weak var viewDelegate: BubbleReactionsViewModelViewDelegate? + + // MARK: - Setup + + @objc init(aggregatedReactions: MXAggregatedReactions, + eventId: String, + showAll: Bool) { + self.aggregatedReactions = aggregatedReactions + self.eventId = eventId + self.showAll = showAll + } + + // MARK: - Public + + func process(viewAction: BubbleReactionsViewAction) { + switch viewAction { + case .loadData: + self.loadData() + case .tapReaction(let index): + guard index < self.aggregatedReactions.reactions.count else { + return + } + let reactionCount = self.aggregatedReactions.reactions[index] + if reactionCount.myUserHasReacted { + self.viewModelDelegate?.bubbleReactionsViewModel(self, didRemoveReaction: reactionCount, forEventId: self.eventId) + } else { + self.viewModelDelegate?.bubbleReactionsViewModel(self, didAddReaction: reactionCount, forEventId: self.eventId) + } + case .addNewReaction: + break + case .tapShowAction(.showAll): + self.viewModelDelegate?.bubbleReactionsViewModel(self, didShowAllTappedForEventId: self.eventId) + case .tapShowAction(.showLess): + self.viewModelDelegate?.bubbleReactionsViewModel(self, didShowLessTappedForEventId: self.eventId) + } + } + + func loadData() { + var reactions = self.aggregatedReactions.reactions + var showAllButtonState: BubbleReactionsViewState.ShowAllButtonState = .none + + // Limit displayed reactions if required + if reactions.count > Constants.maxItemsWhenLimited { + if self.showAll == true { + showAllButtonState = .showLess + } else { + reactions = Array(reactions[0.. BubbleReactionViewData in + return BubbleReactionViewData(emoji: reactionCount.reaction, countString: "\(reactionCount.count)", isCurrentUserReacted: reactionCount.myUserHasReacted) + } + + self.viewDelegate?.bubbleReactionsViewModel(self, didUpdateViewState: .loaded(reactionsViewData: reactionsViewData, showAllButtonState: showAllButtonState)) + } +} diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModelType.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModelType.swift new file mode 100644 index 0000000000..3496854dfa --- /dev/null +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionsViewModelType.swift @@ -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 + +enum BubbleReactionsViewAction { + case loadData + case tapReaction(index: Int) + case addNewReaction + case tapShowAction(action: ShowAction) + + enum ShowAction { + case showAll + case showLess + } +} + +enum BubbleReactionsViewState { + case loaded(reactionsViewData: [BubbleReactionViewData], showAllButtonState: ShowAllButtonState) + + enum ShowAllButtonState { + case none + case showAll + case showLess + } +} + +@objc protocol BubbleReactionsViewModelDelegate: class { + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didAddReaction reactionCount: MXReactionCount, forEventId eventId: String) + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didRemoveReaction reactionCount: MXReactionCount, forEventId eventId: String) + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didShowAllTappedForEventId eventId: String) + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didShowLessTappedForEventId eventId: String) +} + +protocol BubbleReactionsViewModelViewDelegate: class { + func bubbleReactionsViewModel(_ viewModel: BubbleReactionsViewModel, didUpdateViewState viewState: BubbleReactionsViewState) +} + +protocol BubbleReactionsViewModelType { + var viewModelDelegate: BubbleReactionsViewModelDelegate? { get set } + var viewDelegate: BubbleReactionsViewModelViewDelegate? { get set } + + func process(viewAction: BubbleReactionsViewAction) +} diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index dc59bf3a67..777063a598 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -35,6 +35,15 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) */ @property(nonatomic) BOOL containsLastMessage; +/** + Indicate true to display the timestamp of the selected component. + */ +@property(nonatomic) BOOL showTimestampForSelectedComponent; + +/** + Indicate true to display the timestamp of the selected component on the left if possible (YES by default). + */ +@property(nonatomic) BOOL displayTimestampForSelectedComponentOnLeftWhenPossible; /** The event id of the current selected event inside the bubble. Default is nil. @@ -56,4 +65,29 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) */ @property(nonatomic, readonly) NSInteger selectedComponentIndex; +/** + Return additional content height (read receipts, reactions). + */ +@property(nonatomic, readonly) CGFloat additionalContentHeight; + +/** + Indicate to update additional content height. + */ +- (void)setNeedsUpdateAdditionalContentHeight; + +/** + Update additional content height if needed. + */ +- (void)updateAdditionalContentHeightIfNeeded; + +/** + The index of the first visible component. NSNotFound by default. + */ +- (NSInteger)firstVisibleComponentIndex; + +#pragma mark - Show all reactions + +- (BOOL)showAllReactionsForEvent:(NSString*)eventId; +- (void)setShowAllReactions:(BOOL)showAllReactions forEvent:(NSString*)eventId; + @end diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 81cbef55f7..5fda69f74d 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1,5 +1,6 @@ /* Copyright 2015 OpenMarket 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. @@ -21,13 +22,40 @@ #import "AvatarGenerator.h" #import "Tools.h" +#import "Riot-Swift.h" + static NSAttributedString *timestampVerticalWhitespace = nil; -static NSAttributedString *readReceiptVerticalWhitespace = nil; + +@interface RoomBubbleCellData() + +@property(nonatomic, readonly) BOOL addVerticalWhitespaceForSelectedComponentTimestamp; +@property(nonatomic, readwrite) CGFloat additionalContentHeight; +@property(nonatomic) BOOL shouldUpdateAdditionalContentHeight; + +// Flags to "Show All" reactions for an event +@property(nonatomic) NSMutableSet *eventsToShowAllReactions; + +@end @implementation RoomBubbleCellData +- (BOOL)addVerticalWhitespaceForSelectedComponentTimestamp +{ + return self.showTimestampForSelectedComponent && !self.displayTimestampForSelectedComponentOnLeftWhenPossible; +} + #pragma mark - Override MXKRoomBubbleCellData +- (instancetype)init +{ + self = [super init]; + if (self) + { + _eventsToShowAllReactions = [NSMutableSet set]; + } + return self; +} + - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomState andRoomDataSource:(MXKRoomDataSource *)roomDataSource2 { self = [super initWithEvent:event andRoomState:roomState andRoomDataSource:roomDataSource2]; @@ -58,13 +86,11 @@ - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomS // Increase maximum number of components self.maxComponentCount = 20; - - // Initialize read receipts - self.readReceipts = [NSMutableDictionary dictionary]; - self.readReceipts[event.eventId] = [roomDataSource.room getEventReceipts:event.eventId sorted:YES]; // Reset attributedTextMessage to force reset MXKRoomCellData parameters self.attributedTextMessage = nil; + + self.displayTimestampForSelectedComponentOnLeftWhenPossible = YES; } return self; @@ -91,6 +117,8 @@ - (void)prepareBubbleComponentsPosition shouldUpdateComponentsPosition = NO; } + + [self updateAdditionalContentHeightIfNeeded]; } - (NSAttributedString*)attributedTextMessage @@ -199,7 +227,7 @@ - (NSAttributedString*)refreshAttributedTextMessage } // Check whether the timestamp is displayed for this component, and check whether a vertical whitespace is required - if ((selectedComponentIndex == index || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) + if (((selectedComponentIndex == index && self.addVerticalWhitespaceForSelectedComponentTimestamp) || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) { currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; [currentAttributedTextMsg appendAttributedString:componentString]; @@ -210,11 +238,7 @@ - (NSAttributedString*)refreshAttributedTextMessage currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; } - if (self.readReceipts[component.event.eventId].count) - { - // Add vertical whitespace in case of read receipts - [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; - } + [self addVerticalWhitespaceToString:currentAttributedTextMsg forEvent:component.event.eventId]; // The first non empty component has been handled. break; @@ -238,7 +262,7 @@ - (NSAttributedString*)refreshAttributedTextMessage } // Check whether the timestamp is displayed - if (selectedComponentIndex == index || lastMessageIndex == index) + if ((selectedComponentIndex == index && self.addVerticalWhitespaceForSelectedComponentTimestamp) || lastMessageIndex == index) { [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; } @@ -246,33 +270,56 @@ - (NSAttributedString*)refreshAttributedTextMessage // Append attributed text [currentAttributedTextMsg appendAttributedString:componentString]; - if (self.readReceipts[component.event.eventId].count) - { - // Add vertical whitespace in case of read receipts - [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; - } + [self addVerticalWhitespaceToString:currentAttributedTextMsg forEvent:component.event.eventId]; } } return currentAttributedTextMsg; } +- (NSInteger)firstVisibleComponentIndex +{ + __block NSInteger firstVisibleComponentIndex = NSNotFound; + + if (self.attachment && self.bubbleComponents.count) + { + firstVisibleComponentIndex = 0; + } + else + { + [self.bubbleComponents enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + + MXKRoomBubbleComponent *component = (MXKRoomBubbleComponent*)obj; + + if (component.attributedTextMessage) + { + firstVisibleComponentIndex = idx; + *stop = YES; + } + }]; + } + + return firstVisibleComponentIndex; +} + - (void)refreshBubbleComponentsPosition { // CAUTION: This method must be called on the main thread. @synchronized(bubbleComponents) { + NSInteger bubbleComponentsCount = bubbleComponents.count; + // Check whether there is at least one component. - if (bubbleComponents.count) + if (bubbleComponentsCount) { - BOOL hasReadReceipts = NO; - // Set position of the first component CGFloat positionY = (self.attachment == nil || self.attachment.type == MXKAttachmentTypeFile || self.attachment.type == MXKAttachmentTypeAudio) ? MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET : 0; MXKRoomBubbleComponent *component; NSUInteger index = 0; - for (; index < bubbleComponents.count; index++) + + // Use same position for first components without render (redacted) + for (; index < bubbleComponentsCount; index++) { // Compute the vertical position for next component component = bubbleComponents[index]; @@ -281,39 +328,19 @@ - (void)refreshBubbleComponentsPosition if (component.attributedTextMessage) { - hasReadReceipts = (self.readReceipts[component.event.eventId].count > 0); break; } } // Check whether the position of other components need to be refreshed - if (!self.attachment && index < bubbleComponents.count) + if (!self.attachment && index < bubbleComponentsCount) { - NSMutableAttributedString *attributedString; + NSMutableAttributedString *attributedString = [NSMutableAttributedString new]; NSInteger selectedComponentIndex = self.selectedComponentIndex; NSInteger lastMessageIndex = self.containsLastMessage ? self.mostRecentComponentIndex : NSNotFound; - - // Check whether the timestamp is displayed for this first component, and check whether a vertical whitespace is required - if ((selectedComponentIndex == index || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) - { - attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; - [attributedString appendAttributedString:component.attributedTextMessage]; - } - else - { - // Init attributed string with the first text component - attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:component.attributedTextMessage]; - } - - // Vertical whitespace is added in case of read receipts - if (hasReadReceipts) - { - [attributedString appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; - } - - [attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; - - for (index++; index < bubbleComponents.count; index++) + NSInteger visibleMessageIndex = 0; + + for (; index < bubbleComponentsCount; index++) { // Compute the vertical position for next component component = bubbleComponents[index]; @@ -321,17 +348,14 @@ - (void)refreshBubbleComponentsPosition if (component.attributedTextMessage) { // Prepare its attributed string by considering potential vertical margin required to display timestamp. - NSAttributedString *componentString; - if (selectedComponentIndex == index || lastMessageIndex == index) - { - NSMutableAttributedString *componentAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; - [componentAttributedString appendAttributedString:component.attributedTextMessage]; - - componentString = componentAttributedString; - } - else + NSAttributedString *componentString = component.attributedTextMessage; + + // Check whether the timestamp is displayed for this component, and check whether a vertical whitespace is required + + if (((selectedComponentIndex == index && self.addVerticalWhitespaceForSelectedComponentTimestamp) || lastMessageIndex == index) + && !(visibleMessageIndex == 0 && !(self.shouldHideSenderInformation || self.shouldHideSenderName))) { - componentString = component.attributedTextMessage; + [attributedString appendAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; } // Append this attributed string. @@ -345,13 +369,12 @@ - (void)refreshBubbleComponentsPosition component.position = CGPointMake(0, positionY); - // Add vertical whitespace in case of read receipts. - if (self.readReceipts[component.event.eventId].count) - { - [attributedString appendAttributedString:[RoomBubbleCellData readReceiptVerticalWhitespace]]; - } + // Vertical whitespace is added in case of read receipts or reactions + [self addVerticalWhitespaceToString:attributedString forEvent:component.event.eventId]; [attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; + + visibleMessageIndex++; } else { @@ -363,6 +386,142 @@ - (void)refreshBubbleComponentsPosition } } +- (void)addVerticalWhitespaceToString:(NSMutableAttributedString *)attributedString forEvent:(NSString *)eventId +{ + // Add vertical whitespace in case of read receipts. + NSUInteger reactionCount = self.reactions[eventId].reactions.count; + + MXAggregatedReactions *aggregatedReactions = self.reactions[eventId]; + + if (reactionCount) + { + CGFloat bubbleReactionsViewWidth = self.maxTextViewWidth - 4; + + CGSize fittingSize = UILayoutFittingCompressedSize; + fittingSize.width = bubbleReactionsViewWidth; + + static BubbleReactionsView *bubbleReactionsView; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + bubbleReactionsView = [BubbleReactionsView new]; + }); + + BOOL showAllReactions = [self.eventsToShowAllReactions containsObject:eventId]; + + bubbleReactionsView.frame = CGRectMake(0, 0, bubbleReactionsViewWidth, 1.0); + BubbleReactionsViewModel *viemModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:eventId showAll:showAllReactions]; + bubbleReactionsView.viewModel = viemModel; + [bubbleReactionsView setNeedsLayout]; + [bubbleReactionsView layoutIfNeeded]; + + CGFloat height = [bubbleReactionsView systemLayoutSizeFittingSize:fittingSize].height + RoomBubbleCellLayout.reactionsViewTopMargin; + + [attributedString appendAttributedString:[RoomBubbleCellData verticalWhitespaceForHeight: height]]; + } + + // Add vertical whitespace in case of read receipts. + if (self.readReceipts[eventId].count) + { + [attributedString appendAttributedString:[RoomBubbleCellData verticalWhitespaceForHeight:RoomBubbleCellLayout.readReceiptsViewHeight + RoomBubbleCellLayout.readReceiptsViewTopMargin]]; + } +} + +- (CGFloat)computeAdditionalHeight +{ + CGFloat height = 0; + + for (MXKRoomBubbleComponent *bubbleComponent in self.bubbleComponents) + { + NSString *eventId = bubbleComponent.event.eventId; + + height+= [self reactionHeightForEventId:eventId]; + height+= [self readReceiptHeightForEventId:eventId]; + } + + return height; +} + +- (void)updateAdditionalContentHeightIfNeeded; +{ + if (self.shouldUpdateAdditionalContentHeight) + { + void(^updateAdditionalHeight)(void) = ^() { + self.additionalContentHeight = [self computeAdditionalHeight]; + }; + + // The additional height depends on the room read receipts and reactions view which must be calculated on the main thread. + // Check here the current thread, this is just a sanity check because this method is called during the rendering step + // which takes place on the main thread. + if ([NSThread currentThread] != [NSThread mainThread]) + { + NSLog(@"[RoomBubbleCellData] prepareBubbleComponentsPosition called on wrong thread"); + dispatch_sync(dispatch_get_main_queue(), ^{ + updateAdditionalHeight(); + }); + } + else + { + updateAdditionalHeight(); + } + + self.shouldUpdateAdditionalContentHeight = NO; + } +} + +- (void)setNeedsUpdateAdditionalContentHeight +{ + self.shouldUpdateAdditionalContentHeight = YES; +} + +- (CGFloat)reactionHeightForEventId:(NSString*)eventId +{ + CGFloat height = 0; + + NSUInteger reactionCount = self.reactions[eventId].reactions.count; + + MXAggregatedReactions *aggregatedReactions = self.reactions[eventId]; + + if (reactionCount) + { + CGFloat bubbleReactionsViewWidth = self.maxTextViewWidth - 4; + + CGSize fittingSize = UILayoutFittingCompressedSize; + fittingSize.width = bubbleReactionsViewWidth; + + static BubbleReactionsView *bubbleReactionsView; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + bubbleReactionsView = [BubbleReactionsView new]; + }); + + BOOL showAllReactions = [self.eventsToShowAllReactions containsObject:eventId]; + + bubbleReactionsView.frame = CGRectMake(0, 0, bubbleReactionsViewWidth, 1.0); + BubbleReactionsViewModel *viemModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:eventId showAll:showAllReactions]; + bubbleReactionsView.viewModel = viemModel; + [bubbleReactionsView setNeedsLayout]; + [bubbleReactionsView layoutIfNeeded]; + + height = [bubbleReactionsView systemLayoutSizeFittingSize:fittingSize].height + RoomBubbleCellLayout.reactionsViewTopMargin; + } + + return height; +} + +- (CGFloat)readReceiptHeightForEventId:(NSString*)eventId +{ + CGFloat height = 0; + + if (self.readReceipts[eventId].count) + { + height = RoomBubbleCellLayout.readReceiptsViewHeight + RoomBubbleCellLayout.readReceiptsViewTopMargin; + } + + return height; +} + - (void)setContainsLastMessage:(BOOL)containsLastMessage { // Check whether there is something to do @@ -468,18 +627,21 @@ + (NSAttributedString *)timestampVerticalWhitespace return timestampVerticalWhitespace; } - -+ (NSAttributedString *)readReceiptVerticalWhitespace ++ (NSAttributedString *)verticalWhitespaceForHeight:(CGFloat)height { - @synchronized(self) + UIFont *sizingFont = [UIFont systemFontOfSize:2]; + CGFloat returnHeight = sizingFont.lineHeight; + + NSUInteger returns = (NSUInteger)round(height/returnHeight); + NSMutableString *returnString = [NSMutableString string]; + + for (NSUInteger i = 0; i < returns; i++) { - if (readReceiptVerticalWhitespace == nil) - { - readReceiptVerticalWhitespace = [[NSAttributedString alloc] initWithString:@"\n\n" attributes:@{NSForegroundColorAttributeName : [UIColor blackColor], - NSFontAttributeName: [UIFont systemFontOfSize:4]}]; - } + [returnString appendString:@"\n"]; } - return readReceiptVerticalWhitespace; + + return [[NSAttributedString alloc] initWithString:returnString attributes:@{NSForegroundColorAttributeName : [UIColor blackColor], + NSFontAttributeName: sizingFont}]; } - (BOOL)hasSameSenderAsBubbleCellData:(id)bubbleCellData @@ -513,10 +675,27 @@ - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState return NO; } - // Update read receipts for this bubble - self.readReceipts[event.eventId] = [roomDataSource.room getEventReceipts:event.eventId sorted:YES]; - return [super addEvent:event andRoomState:roomState]; } + +#pragma mark - Show all reactions + +- (BOOL)showAllReactionsForEvent:(NSString*)eventId +{ + return [self.eventsToShowAllReactions containsObject:eventId]; +} + +- (void)setShowAllReactions:(BOOL)showAllReactions forEvent:(NSString*)eventId +{ + if (showAllReactions) + { + [self.eventsToShowAllReactions addObject:eventId]; + } + else + { + [self.eventsToShowAllReactions removeObject:eventId]; + } +} + @end diff --git a/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift new file mode 100644 index 0000000000..8a0147e5f5 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift @@ -0,0 +1,172 @@ +/* + 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 ContextualMenuItemView: UIView, NibOwnerLoadable { + + // MARK: - Constants + + private enum ColorAlpha { + static let normal: CGFloat = 1.0 + static let highlighted: CGFloat = 0.3 + } + + private enum ViewAlpha { + static let normal: CGFloat = 1.0 + static let disabled: CGFloat = 0.3 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var imageView: UIImageView! + @IBOutlet private weak var titleLabel: UILabel! + + // MARK: Private + + private var originalImage: UIImage? + + private var isHighlighted: Bool = false { + didSet { + self.updateView() + } + } + + // MARK: Public + + var titleColor: UIColor = .black { + didSet { + self.updateView() + } + } + + var imageColor: UIColor = .black { + didSet { + self.updateView() + } + } + + var isEnabled: Bool = true { + didSet { + self.updateView() + } + } + + var action: (() -> Void)? + + // MARK: Setup + + private func commonInit() { + self.setupGestureRecognizer() + } + + convenience init() { + self.init(frame: CGRect.zero) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.loadNibContent() + self.commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.loadNibContent() + self.commonInit() + } + + // MARK: - Public + + func fill(title: String, image: UIImage?) { + self.originalImage = image?.withRenderingMode(.alwaysTemplate) + self.titleLabel.text = title + self.updateView() + } + + func fill(menuItem: RoomContextualMenuItem) { + self.fill(title: menuItem.title, image: menuItem.image) + self.action = menuItem.action + self.isEnabled = menuItem.isEnabled + } + + // MARK: - Private + + private func setupGestureRecognizer() { + let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(buttonAction(_:))) + gestureRecognizer.minimumPressDuration = 0 + self.addGestureRecognizer(gestureRecognizer) + } + + private func updateView() { + + let viewAlpha = self.isEnabled ? ViewAlpha.normal : ViewAlpha.disabled + let colorAlpha = self.isHighlighted ? ColorAlpha.highlighted : ColorAlpha.normal + + self.updateTitleAndImageAlpha(viewAlpha) + self.imageView.tintColor = self.imageColor + self.updateTitleAndImageColorAlpha(colorAlpha) + } + + private func updateTitleAndImageAlpha(_ alpha: CGFloat) { + self.imageView.alpha = alpha + self.titleLabel.alpha = alpha + } + + private func updateTitleAndImageColorAlpha(_ alpha: CGFloat) { + let titleColor: UIColor + let image: UIImage? + + if alpha < 1.0 { + titleColor = self.titleColor.withAlphaComponent(alpha) + image = self.originalImage?.vc_tintedImage(usingColor: self.imageColor.withAlphaComponent(alpha)) + } else { + titleColor = self.titleColor + image = self.originalImage + } + + self.titleLabel.textColor = titleColor + self.imageView.image = image + } + + // MARK: - Actions + + @objc private func buttonAction(_ sender: UILongPressGestureRecognizer) { + guard self.isEnabled else { + return + } + + let isBackgroundViewTouched = sender.vc_isTouchingInside() + + switch sender.state { + case .began, .changed: + self.isHighlighted = isBackgroundViewTouched + case .ended: + self.isHighlighted = false + + if isBackgroundViewTouched { + self.action?() + } + case .cancelled: + self.isHighlighted = false + default: + break + } + } +} diff --git a/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.xib b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.xib new file mode 100644 index 0000000000..0032099d7f --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.xib @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionMenuItemViewData.swift b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionMenuItemViewData.swift new file mode 100644 index 0000000000..df9f4ed27c --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionMenuItemViewData.swift @@ -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 + +struct ReactionMenuItemViewData { + let emoji: String + let isSelected: Bool +} diff --git a/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuButton.swift b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuButton.swift new file mode 100644 index 0000000000..6da7eb19bf --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuButton.swift @@ -0,0 +1,85 @@ +/* + 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 + +class ReactionsMenuButton: UIButton, Themable { + + // MARK: - Constants + + private enum Constants { + static let borderWidthSelected: CGFloat = 1/UIScreen.main.scale + static let borderColorAlpha: CGFloat = 0.15 + } + + // MARK: - Properties + + private var theme: Theme! + + // MARK: - Setup + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.commonInit() + } + + // MARK: - Life cycle + + override func layoutSubviews() { + super.layoutSubviews() + self.layer.cornerRadius = self.frame.size.height / 3 + self.layer.borderWidth = self.isSelected ? Constants.borderWidthSelected : 0 + } + + // MARK: - Private + + private func commonInit() { + self.layer.masksToBounds = true + + self.update(theme: ThemeService.shared().theme) + + customizeViewRendering() + updateView() + } + + private func customizeViewRendering() { + self.tintColor = UIColor.clear + } + + func update(theme: Theme) { + self.theme = theme + + self.setTitleColor(self.theme.textPrimaryColor, for: .normal) + self.setTitleColor(self.theme.textPrimaryColor, for: .selected) + + self.layer.borderColor = self.theme.tintColor.withAlphaComponent(Constants.borderColorAlpha).cgColor + } + + private func updateView() { + backgroundColor = isSelected ? self.theme.tintBackgroundColor : UIColor.clear + } + + override open var isSelected: Bool { + didSet { + self.updateView() + } + } +} diff --git a/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuView.swift b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuView.swift new file mode 100644 index 0000000000..5ceb40d9b2 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuView.swift @@ -0,0 +1,146 @@ +/* + 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 ReactionsMenuView: UIView, Themable, NibLoadable { + + // MARK: - Constants + + private enum Constants { + static let selectedReactionAnimationScale: CGFloat = 1.2 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var reactionsBackgroundView: UIView! + @IBOutlet private weak var reactionsStackView: UIStackView! + + // MARK: Private + + private var reactionViewDatas: [ReactionMenuItemViewData] = [] + private var reactionButtons: [ReactionsMenuButton] = [] + private var tappedReactionButton: ReactionsMenuButton? + + // MARK: Public + + var viewModel: ReactionsMenuViewModelType? { + didSet { + self.viewModel?.viewDelegate = self + self.viewModel?.process(viewAction: .loadData) + } + } + + var reactionHasBeenTapped: Bool { + return self.tappedReactionButton != nil + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + self.reactionsBackgroundView.layer.masksToBounds = true + self.update(theme: ThemeService.shared().theme) + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.reactionsBackgroundView.layer.cornerRadius = self.reactionsBackgroundView.frame.size.height/2 + } + + // MARK: - Public + + func update(theme: Theme) { + self.reactionsBackgroundView.backgroundColor = theme.headerBackgroundColor + } + + func selectionAnimationInstructionPart1() { + guard let tappedButton = self.tappedReactionButton else { + return + } + let scale = Constants.selectedReactionAnimationScale + tappedButton.superview?.bringSubviewToFront(tappedButton) + tappedButton.transform = CGAffineTransform(scaleX: scale, y: scale) + } + + func selectionAnimationInstructionPart2() { + guard let tappedButton = self.tappedReactionButton else { + return + } + tappedButton.transform = CGAffineTransform.identity + tappedButton.isSelected.toggle() + } + + // MARK: - Private + + private func fill(reactionsMenuViewDatas: [ReactionMenuItemViewData]) { + self.reactionViewDatas = reactionsMenuViewDatas + + self.reactionsStackView.vc_removeAllSubviews() + + let reactionsStackViewCount = self.reactionsStackView.arrangedSubviews.count + + // Remove all menu buttons if reactions count has changed + if reactionsStackViewCount != self.reactionViewDatas.count { + self.reactionsStackView.vc_removeAllSubviews() + } + + var index = 0 + + for reactionViewData in self.reactionViewDatas { + + let reactionsMenuButton: ReactionsMenuButton + + if index < reactionsStackViewCount, let foundReactionsMenuButton = self.reactionsStackView.arrangedSubviews[index] as? ReactionsMenuButton { + reactionsMenuButton = foundReactionsMenuButton + } else { + reactionsMenuButton = ReactionsMenuButton() + reactionsMenuButton.addTarget(self, action: #selector(reactionButtonAction), for: .touchUpInside) + self.reactionsStackView.addArrangedSubview(reactionsMenuButton) + self.reactionButtons.append(reactionsMenuButton) + } + + reactionsMenuButton.setTitle(reactionViewData.emoji, for: .normal) + reactionsMenuButton.isSelected = reactionViewData.isSelected + + index+=1 + } + } + + @objc private func reactionButtonAction(_ sender: ReactionsMenuButton) { + guard let tappedReaction = sender.titleLabel?.text else { + return + } + self.tappedReactionButton = sender + self.viewModel?.process(viewAction: .tap(reaction: tappedReaction)) + } +} + +// MARK: - ReactionsMenuViewModelViewDelegate +extension ReactionsMenuView: ReactionsMenuViewModelViewDelegate { + + func reactionsMenuViewModel(_ viewModel: ReactionsMenuViewModel, didUpdateViewState viewState: ReactionsMenuViewState) { + switch viewState { + case .loaded(reactionsViewData: let reactionsViewData): + self.fill(reactionsMenuViewDatas: reactionsViewData) + } + } +} diff --git a/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuView.xib b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuView.xib new file mode 100644 index 0000000000..f9c6c01133 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuView.xib @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewAction.swift b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewAction.swift new file mode 100644 index 0000000000..3608a91876 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewAction.swift @@ -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 + +/// Action chosen by the user +enum ReactionsMenuViewAction { + case loadData + case tap(reaction: String) +} diff --git a/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewModel.swift b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewModel.swift new file mode 100644 index 0000000000..c35678cdb6 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewModel.swift @@ -0,0 +1,84 @@ +/* + 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 final class ReactionsMenuViewModel: NSObject, ReactionsMenuViewModelType { + + // MARK: - Properties + + private let reactions = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"] + private var currentViewDatas: [ReactionMenuItemViewData] = [] + + // MARK: Private + + private let aggregatedReactions: MXAggregatedReactions? + private let reactionsViewData: [ReactionMenuItemViewData] = [] + private let eventId: String + + // MARK: Public + + @objc weak var coordinatorDelegate: ReactionsMenuViewModelCoordinatorDelegate? + weak var viewDelegate: ReactionsMenuViewModelViewDelegate? + + // MARK: - Setup + + @objc init(aggregatedReactions: MXAggregatedReactions?, + eventId: String) { + self.aggregatedReactions = aggregatedReactions + self.eventId = eventId + } + + // MARK: - Public + + func process(viewAction: ReactionsMenuViewAction) { + switch viewAction { + case .loadData: + self.loadData() + case .tap(let reaction): + if let viewData = self.currentViewDatas.first(where: { $0.emoji == reaction }) { + if viewData.isSelected { + self.coordinatorDelegate?.reactionsMenuViewModel(self, didRemoveReaction: reaction, forEventId: self.eventId) + } else { + self.coordinatorDelegate?.reactionsMenuViewModel(self, didAddReaction: reaction, forEventId: self.eventId) + } + } + } + } + + // MARK: - Private + + private func loadData() { + let reactionCounts = self.aggregatedReactions?.withNonZeroCount()?.reactions ?? [] + + var quickReactionsWithUserReactedFlag: [String: Bool] = Dictionary(uniqueKeysWithValues: self.reactions.map { ($0, false) }) + + reactionCounts.forEach { (reactionCount) in + if let hasUserReacted = quickReactionsWithUserReactedFlag[reactionCount.reaction], hasUserReacted == false { + quickReactionsWithUserReactedFlag[reactionCount.reaction] = reactionCount.myUserHasReacted + } + } + + let reactionMenuItemViewDatas: [ReactionMenuItemViewData] = self.reactions.map { reaction -> ReactionMenuItemViewData in + let isSelected = quickReactionsWithUserReactedFlag[reaction] ?? false + return ReactionMenuItemViewData(emoji: reaction, isSelected: isSelected) + } + + self.currentViewDatas = reactionMenuItemViewDatas + + self.viewDelegate?.reactionsMenuViewModel(self, didUpdateViewState: ReactionsMenuViewState.loaded(reactionsViewData: reactionMenuItemViewDatas)) + } +} diff --git a/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewModelType.swift b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewModelType.swift new file mode 100644 index 0000000000..0e162f6c1f --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewModelType.swift @@ -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 Foundation + +protocol ReactionsMenuViewModelViewDelegate: class { + func reactionsMenuViewModel(_ viewModel: ReactionsMenuViewModel, didUpdateViewState viewState: ReactionsMenuViewState) +} + +@objc protocol ReactionsMenuViewModelCoordinatorDelegate: class { + func reactionsMenuViewModel(_ viewModel: ReactionsMenuViewModel, didAddReaction reaction: String, forEventId eventId: String) + func reactionsMenuViewModel(_ viewModel: ReactionsMenuViewModel, didRemoveReaction reaction: String, forEventId eventId: String) +} + +protocol ReactionsMenuViewModelType { + + var coordinatorDelegate: ReactionsMenuViewModelCoordinatorDelegate? { get set } + var viewDelegate: ReactionsMenuViewModelViewDelegate? { get set } + + func process(viewAction: ReactionsMenuViewAction) +} diff --git a/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewState.swift b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewState.swift new file mode 100644 index 0000000000..e74a77ef7b --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/ReactionsMenu/ReactionsMenuViewState.swift @@ -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 + +/// ReactionsMenuView view state +enum ReactionsMenuViewState { + case loaded(reactionsViewData: [ReactionMenuItemViewData]) +} diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift new file mode 100644 index 0000000000..f4898fca04 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift @@ -0,0 +1,62 @@ +/* + 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 enum RoomContextualMenuAction: Int { + case copy + case reply + case edit + case more + + // MARK: - Properties + + var title: String { + let title: String + + switch self { + case .copy: + title = VectorL10n.roomEventActionCopy + case .reply: + title = VectorL10n.roomEventActionReply + case .edit: + title = VectorL10n.roomEventActionEdit + case .more: + title = VectorL10n.roomEventActionMore + } + + return title + } + + var image: UIImage? { + let image: UIImage? + + switch self { + case .copy: + image = Asset.Images.roomContextMenuCopy.image + case .reply: + image = Asset.Images.roomContextMenuReply.image + case .edit: + image = Asset.Images.roomContextMenuEdit.image + case .more: + image = Asset.Images.roomContextMenuMore.image + default: + image = nil + } + + return image + } +} diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuItem.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuItem.swift new file mode 100644 index 0000000000..ba32691f3e --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuItem.swift @@ -0,0 +1,37 @@ +/* + 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 + +@objcMembers +final class RoomContextualMenuItem: NSObject { + + // MARK: - Properties + + let title: String + let image: UIImage? + + var isEnabled: Bool = true + var action: (() -> Void)? + + // MARK: - Setup + + init(menuAction: RoomContextualMenuAction) { + self.title = menuAction.title + self.image = menuAction.image + super.init() + } +} diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuPresenter.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuPresenter.swift new file mode 100644 index 0000000000..04d87ad924 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuPresenter.swift @@ -0,0 +1,130 @@ +/* + 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 + +@objcMembers +final class RoomContextualMenuPresenter: NSObject { + + // MARK: - Constants + + private enum AnimationDurations { + static let showMenu: TimeInterval = 0.15 + static let showMenuFromSingleTap: TimeInterval = 0.1 + static let hideMenu: TimeInterval = 0.2 + static let selectedReaction: TimeInterval = 0.15 + } + + // MARK: - Properties + + // MARK: Private + + private weak var roomContextualMenuViewController: RoomContextualMenuViewController? + + // MARK: Public + + var isPresenting: Bool { + return self.roomContextualMenuViewController?.parent != nil + } + + // MARK: - Public + + func present(roomContextualMenuViewController: RoomContextualMenuViewController, + from viewController: UIViewController, + on view: UIView, + contentToReactFrame: CGRect, // Not nullable for compatibility with Obj-C + fromSingleTapGesture usedSingleTapGesture: Bool, + animated: Bool, + completion: (() -> Void)?) { + guard self.isPresenting == false else { + return + } + + viewController.vc_addChildViewController(viewController: roomContextualMenuViewController, onView: view) + + self.roomContextualMenuViewController = roomContextualMenuViewController + + roomContextualMenuViewController.contentToReactFrame = contentToReactFrame + + roomContextualMenuViewController.hideMenuToolbar() + roomContextualMenuViewController.prepareReactionsMenuAnimations() + roomContextualMenuViewController.hideReactionsMenu() + + roomContextualMenuViewController.view.layoutIfNeeded() + + let animationInstructions: (() -> Void) = { + roomContextualMenuViewController.showMenuToolbar() + roomContextualMenuViewController.showReactionsMenu() + roomContextualMenuViewController.view.layoutIfNeeded() + } + + if animated { + let animationDuration = usedSingleTapGesture ? AnimationDurations.showMenuFromSingleTap : AnimationDurations.showMenu + + UIView.animate(withDuration: animationDuration, animations: { + animationInstructions() + }, completion: { completed in + completion?() + }) + } else { + animationInstructions() + completion?() + } + } + + func hideContextualMenu(animated: Bool, completion: (() -> Void)?) { + guard let roomContextualMenuViewController = self.roomContextualMenuViewController, self.isPresenting else { + completion?() + return + } + + let animationInstructions: (() -> Void) = { + roomContextualMenuViewController.hideMenuToolbar() + roomContextualMenuViewController.hideReactionsMenu() + roomContextualMenuViewController.view.layoutIfNeeded() + } + + let animationCompletionInstructions: (() -> Void) = { + roomContextualMenuViewController.vc_removeFromParent() + self.roomContextualMenuViewController = nil + completion?() + } + + if animated { + if roomContextualMenuViewController.shouldPerformTappedReactionAnimation { + UIView.animate(withDuration: AnimationDurations.selectedReaction, animations: { + roomContextualMenuViewController.selectedReactionAnimationsIntructionsPart1() + }, completion: { _ in + UIView.animate(withDuration: AnimationDurations.hideMenu, animations: { + roomContextualMenuViewController.selectedReactionAnimationsIntructionsPart2() + animationInstructions() + }, completion: { completed in + animationCompletionInstructions() + }) + }) + } else { + UIView.animate(withDuration: AnimationDurations.hideMenu, animations: { + animationInstructions() + }, completion: { completed in + animationCompletionInstructions() + }) + } + } else { + animationInstructions() + animationCompletionInstructions() + } + } +} diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.swift new file mode 100644 index 0000000000..8d695c9454 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.swift @@ -0,0 +1,141 @@ +/* + 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 RoomContextualMenuToolbarView: MXKRoomInputToolbarView, NibOwnerLoadable, Themable { + + // MARK: - Constants + + private enum Constants { + static let menuItemMinWidth: CGFloat = 50.0 + static let menuItemMaxWidth: CGFloat = 80.0 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var menuItemsStackView: UIStackView! + @IBOutlet private weak var separatorView: UIView! + + // MARK: Private + + private var theme: Theme? + private var menuItemViews: [ContextualMenuItemView] = [] + + // MARK: - Public + + @objc func update(theme: Theme) { + self.theme = theme + self.backgroundColor = theme.backgroundColor + self.tintColor = theme.tintColor + self.separatorView.backgroundColor = theme.lineBreakColor + + for menuItemView in self.menuItemViews { + menuItemView.titleColor = theme.tintColor + menuItemView.imageColor = theme.tintColor + } + } + + @objc func fill(contextualMenuItems: [RoomContextualMenuItem]) { + self.menuItemsStackView.vc_removeAllSubviews() + self.menuItemViews.removeAll() + + for menuItem in contextualMenuItems { + let menuItemView = ContextualMenuItemView() + menuItemView.fill(menuItem: menuItem) + + if let theme = theme { + menuItemView.titleColor = theme.textPrimaryColor + menuItemView.imageColor = theme.tintColor + } + + self.add(menuItemView: menuItemView) + } + + self.layoutIfNeeded() + } + + // MARK: - Setup + + private func commonInit() { + } + + convenience init() { + self.init(frame: CGRect.zero) + self.loadNibContent() + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.loadNibContent() + commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.loadNibContent() + commonInit() + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + } + + // MARK: - Private + + private func add(menuItemView: ContextualMenuItemView) { + let menuItemContentView = UIView() + menuItemContentView.backgroundColor = .clear + + self.add(menuItemView: menuItemView, on: menuItemContentView) + + self.menuItemsStackView.addArrangedSubview(menuItemContentView) + + let widthConstraint = menuItemContentView.widthAnchor.constraint(equalTo: self.menuItemsStackView.widthAnchor) + widthConstraint.priority = .defaultLow + widthConstraint.isActive = true + + self.menuItemViews.append(menuItemView) + } + + private func add(menuItemView: ContextualMenuItemView, on contentView: UIView) { + contentView.translatesAutoresizingMaskIntoConstraints = false + menuItemView.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(menuItemView) + + menuItemView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true + menuItemView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true + + let widthConstraint = menuItemView.widthAnchor.constraint(equalToConstant: 0.0) + widthConstraint.priority = .defaultLow + widthConstraint.isActive = true + + let minWidthConstraint = menuItemView.widthAnchor.constraint(greaterThanOrEqualToConstant: Constants.menuItemMinWidth) + minWidthConstraint.priority = .required + minWidthConstraint.isActive = true + + let maxWidthConstraint = menuItemView.widthAnchor.constraint(lessThanOrEqualToConstant: Constants.menuItemMaxWidth) + maxWidthConstraint.priority = .required + maxWidthConstraint.isActive = true + } +} diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.xib b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.xib new file mode 100644 index 0000000000..42b2b9ab3f --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuToolbarView.xib @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.storyboard b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.storyboard new file mode 100644 index 0000000000..f473449964 --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.storyboard @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift new file mode 100644 index 0000000000..baba7f279c --- /dev/null +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift @@ -0,0 +1,267 @@ +/* + 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 RoomContextualMenuViewControllerDelegate: class { + func roomContextualMenuViewControllerDidTapBackgroundOverlay(_ viewController: RoomContextualMenuViewController) +} + +@objcMembers +final class RoomContextualMenuViewController: UIViewController, Themable { + + // MARK: - Constants + + private enum Constants { + static let reactionsMenuViewVerticalMargin: CGFloat = 10.0 + static let reactionsMenuViewHiddenScale: CGFloat = 0.97 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var backgroundOverlayView: UIView! + @IBOutlet private weak var menuToolbarView: RoomContextualMenuToolbarView! + + @IBOutlet private weak var menuToolbarViewHeightConstraint: NSLayoutConstraint! + @IBOutlet private weak var menuToolbarViewBottomConstraint: NSLayoutConstraint! + + @IBOutlet private weak var reactionsMenuContainerView: UIView! + @IBOutlet private weak var reactionsMenuViewHeightConstraint: NSLayoutConstraint! + @IBOutlet private weak var reactionsMenuViewBottomConstraint: NSLayoutConstraint! + + // MARK: Private + + private var theme: Theme! + private var contextualMenuItems: [RoomContextualMenuItem] = [] + private var reactionsMenuViewModel: ReactionsMenuViewModel? + + private weak var reactionsMenuView: ReactionsMenuView? + + private var reactionsMenuViewBottomStartConstraintConstant: CGFloat? + private var reactionsMenuViewBottomEndConstraintConstant: CGFloat? + + private var hiddenToolbarViewBottomConstant: CGFloat { + let bottomSafeAreaHeight: CGFloat + + if #available(iOS 11.0, *) { + bottomSafeAreaHeight = self.view.safeAreaInsets.bottom + } else { + bottomSafeAreaHeight = self.bottomLayoutGuide.length + } + + return -(self.menuToolbarViewHeightConstraint.constant + bottomSafeAreaHeight) + } + + private var shouldPresentReactionsMenu: Bool { + return self.reactionsMenuContainerView.isHidden == false + } + + // MARK: Public + + var contentToReactFrame: CGRect? + var shouldPerformTappedReactionAnimation: Bool { + return self.reactionsMenuView?.reactionHasBeenTapped ?? false + } + + weak var delegate: RoomContextualMenuViewControllerDelegate? + + // MARK: - Setup + + class func instantiate() -> RoomContextualMenuViewController { + let viewController = StoryboardScene.RoomContextualMenuViewController.initialScene.instantiate() + viewController.theme = ThemeService.shared().theme + return viewController + } + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + self.backgroundOverlayView.isUserInteractionEnabled = true + self.setupBackgroundOverlayGestureRecognizers() + + self.updateViews() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + } + + // MARK: - Public + + func update(contextualMenuItems: [RoomContextualMenuItem], reactionsMenuViewModel: ReactionsMenuViewModel?) { + self.contextualMenuItems = contextualMenuItems + self.reactionsMenuViewModel = reactionsMenuViewModel + if self.isViewLoaded { + self.updateViews() + } + } + + func showMenuToolbar() { + self.menuToolbarViewBottomConstraint.constant = 0 + self.menuToolbarView.alpha = 1 + } + + func hideMenuToolbar() { + self.menuToolbarViewBottomConstraint.constant = self.hiddenToolbarViewBottomConstant + self.menuToolbarView.alpha = 0 + } + + func prepareReactionsMenuAnimations() { + guard let frame = self.contentToReactFrame, frame.equalTo(CGRect.null) == false else { + return + } + + let menuHeight = self.reactionsMenuViewHeightConstraint.constant + let verticalMargin = Constants.reactionsMenuViewVerticalMargin + + let reactionsMenuViewBottomStartConstraintConstant: CGFloat? + let reactionsMenuViewBottomEndConstraintConstant: CGFloat? + + // Try to display the menu at the top of the message first + // Then, try at the bottom + // Else, keep the position defined in the storyboard + if frame.origin.y - verticalMargin >= menuHeight { + let menuViewBottomY = frame.origin.y - verticalMargin + reactionsMenuViewBottomStartConstraintConstant = menuViewBottomY + menuHeight/2 + reactionsMenuViewBottomEndConstraintConstant = menuViewBottomY + } else { + let frameBottomY = frame.origin.y + frame.size.height + verticalMargin + let visibleViewHeight = self.view.frame.size.height - self.menuToolbarView.frame.size.height + + if frameBottomY + menuHeight < visibleViewHeight { + let menuViewBottomY = frameBottomY + menuHeight + + reactionsMenuViewBottomEndConstraintConstant = menuViewBottomY + reactionsMenuViewBottomStartConstraintConstant = menuViewBottomY - menuHeight/2 + } else { + reactionsMenuViewBottomEndConstraintConstant = nil + reactionsMenuViewBottomStartConstraintConstant = nil + } + } + + self.reactionsMenuViewBottomStartConstraintConstant = reactionsMenuViewBottomStartConstraintConstant + self.reactionsMenuViewBottomEndConstraintConstant = reactionsMenuViewBottomEndConstraintConstant + + self.reactionsMenuContainerView.isHidden = false + } + + func showReactionsMenu() { + guard self.shouldPresentReactionsMenu, let reactionsMenuView = self.reactionsMenuView else { + return + } + + if let reactionsMenuViewBottomEndConstraintConstant = self.reactionsMenuViewBottomEndConstraintConstant { + self.reactionsMenuViewBottomConstraint.constant = reactionsMenuViewBottomEndConstraintConstant + } + + reactionsMenuView.alpha = 1 + reactionsMenuContainerView.transform = CGAffineTransform.identity + } + + func hideReactionsMenu() { + guard self.shouldPresentReactionsMenu, let reactionsMenuView = self.reactionsMenuView else { + return + } + + if let reactionsMenuViewBottomStartConstraintConstant = self.reactionsMenuViewBottomStartConstraintConstant { + self.reactionsMenuViewBottomConstraint.constant = reactionsMenuViewBottomStartConstraintConstant + } + + reactionsMenuView.alpha = 0 + + let transformScale = Constants.reactionsMenuViewHiddenScale + self.reactionsMenuContainerView.transform = CGAffineTransform(scaleX: transformScale, y: transformScale) + } + + func selectedReactionAnimationsIntructionsPart1() { + self.reactionsMenuView?.selectionAnimationInstructionPart1() + } + + func selectedReactionAnimationsIntructionsPart2() { + self.reactionsMenuView?.selectionAnimationInstructionPart2() + } + + func update(theme: Theme) { + self.menuToolbarView.update(theme: theme) + } + + // MARK: - Private + + private func updateViews() { + self.menuToolbarView.fill(contextualMenuItems: self.contextualMenuItems) + + let hideReactionMenu: Bool + + if let reactionsMenuViewModel = self.reactionsMenuViewModel { + hideReactionMenu = false + self.updateReactionsMenu(with: reactionsMenuViewModel) + } else { + hideReactionMenu = true + } + + self.reactionsMenuContainerView.isHidden = hideReactionMenu + } + + private func updateReactionsMenu(with viewModel: ReactionsMenuViewModel) { + + if self.reactionsMenuContainerView.subviews.isEmpty { + let reactionsMenuView = ReactionsMenuView.loadFromNib() + self.reactionsMenuContainerView.vc_addSubViewMatchingParent(reactionsMenuView) + self.reactionsMenuView = reactionsMenuView + } + + self.reactionsMenuView?.viewModel = viewModel + } + + private func setupBackgroundOverlayGestureRecognizers() { + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handle(gestureRecognizer:))) + tapGestureRecognizer.delegate = self + + let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(handle(gestureRecognizer:))) + swipeGestureRecognizer.direction = [.down, .up] + swipeGestureRecognizer.delegate = self + + self.backgroundOverlayView.addGestureRecognizer(tapGestureRecognizer) + self.backgroundOverlayView.addGestureRecognizer(swipeGestureRecognizer) + } + + @objc private func handle(gestureRecognizer: UIGestureRecognizer) { + self.delegate?.roomContextualMenuViewControllerDidTapBackgroundOverlay(self) + } + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } +} + +// MARK: - UIGestureRecognizerDelegate +extension RoomContextualMenuViewController: UIGestureRecognizerDelegate { + + // Avoid triggering background overlay gesture recognizers when touching reactions menu + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + return touch.vc_isInside(view: self.reactionsMenuContainerView) == false + } +} diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.h b/Riot/Modules/Room/DataSources/RoomDataSource.h index dc695ba7c8..1c7dfda5ac 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.h +++ b/Riot/Modules/Room/DataSources/RoomDataSource.h @@ -34,6 +34,11 @@ */ @property(nonatomic) BOOL markTimelineInitialEvent; +/** + Tell whether timestamp should be displayed on event selection. Default is YES. + */ +@property(nonatomic) BOOL showBubbleDateTimeOnSelection; + /** Check if there is an active jitsi widget in the room and return it. @@ -41,4 +46,20 @@ */ - (Widget *)jitsiWidget; +/** + Send a video to the room. + Note: Move this method to MatrixKit when MatrixKit project will handle Swift module. + + While sending, a fake event will be echoed in the messages list. + Once complete, this local echo will be replaced by the event saved by the homeserver. + + @param videoLocalURL the local filesystem path of the video to send. + @param success A block object called when the operation succeeds. It returns + the event id of the event generated on the homeserver + @param failure A block object called when the operation fails. + */ +- (void)sendVideo:(NSURL*)videoLocalURL + success:(void (^)(NSString *eventId))success + failure:(void (^)(NSError *error))failure; + @end diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 49d189283b..fa7034b453 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -27,7 +27,9 @@ #import "MXRoom+Riot.h" -@interface RoomDataSource() + + +@interface RoomDataSource() { // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; @@ -58,6 +60,9 @@ - (instancetype)initWithRoomId:(NSString *)roomId andMatrixSession:(MXSession *) self.markTimelineInitialEvent = NO; + self.showBubbleDateTimeOnSelection = YES; + self.showReactions = YES; + // Observe user interface theme change. kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -115,87 +120,29 @@ - (void)destroy [super destroy]; } -- (void)didReceiveReceiptEvent:(MXEvent *)receiptEvent roomState:(MXRoomState *)roomState +- (void)updateCellDataReactions:(id)cellData forEventId:(NSString*)eventId { - // Do the processing on the same processing queue as MXKRoomDataSource - dispatch_async(MXKRoomDataSource.processingQueue, ^{ - - // Remove the previous displayed read receipt for each user who sent a - // new read receipt. - // To implement it, we need to find the sender id of each new read receipt - // among the read receipts array of all events in all bubbles. - NSArray *readReceiptSenders = receiptEvent.readReceiptSenders; - - @synchronized(bubbles) - { - for (RoomBubbleCellData *cellData in bubbles) - { - NSMutableDictionary *> *updatedCellDataReadReceipts = [NSMutableDictionary dictionary]; - - for (NSString *eventId in cellData.readReceipts) - { - for (MXReceiptData *receiptData in cellData.readReceipts[eventId]) - { - for (NSString *senderId in readReceiptSenders) - { - if ([receiptData.userId isEqualToString:senderId]) - { - if (!updatedCellDataReadReceipts[eventId]) - { - updatedCellDataReadReceipts[eventId] = cellData.readReceipts[eventId]; - } - - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"userId!=%@", receiptData.userId]; - updatedCellDataReadReceipts[eventId] = [updatedCellDataReadReceipts[eventId] filteredArrayUsingPredicate:predicate]; - break; - } - } - - } - } + [super updateCellDataReactions:cellData forEventId:eventId]; - // Flush found changed to the cell data - for (NSString *eventId in updatedCellDataReadReceipts) - { - if (updatedCellDataReadReceipts[eventId].count) - { - cellData.readReceipts[eventId] = updatedCellDataReadReceipts[eventId]; - } - else - { - cellData.readReceipts[eventId] = nil; - } - } - } - } + [self setNeedsUpdateAdditionalContentHeightForCellData:cellData]; +} - // Update cell data we have received a read receipt for - NSArray *readEventIds = receiptEvent.readReceiptEventIds; - for (NSString* eventId in readEventIds) - { - RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventId]; - if (cellData) - { - @synchronized(bubbles) - { - if (!cellData.hasNoDisplay) - { - cellData.readReceipts[eventId] = [self.room getEventReceipts:eventId sorted:YES]; - } - else - { - // Ignore the read receipts on the events without an actual display. - cellData.readReceipts[eventId] = nil; - } - } - } - } +- (void)updateCellData:(MXKRoomBubbleCellData*)cellData withReadReceipts:(NSArray*)readReceipts forEventId:(NSString*)eventId +{ + [super updateCellData:cellData withReadReceipts:readReceipts forEventId:eventId]; + + [self setNeedsUpdateAdditionalContentHeightForCellData:cellData]; +} - dispatch_async(dispatch_get_main_queue(), ^{ - // TODO: Be smarter and update only updated cells - [super didReceiveReceiptEvent:receiptEvent roomState:roomState]; - }); - }); +- (void)setNeedsUpdateAdditionalContentHeightForCellData:(id)cellData +{ + RoomBubbleCellData *roomBubbleCellData; + + if ([cellData isKindOfClass:[RoomBubbleCellData class]]) + { + roomBubbleCellData = (RoomBubbleCellData*)cellData; + [roomBubbleCellData setNeedsUpdateAdditionalContentHeight]; + } } #pragma mark - @@ -260,21 +207,83 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N [bubbleCell addTimestampLabelForComponent:cellData.mostRecentComponentIndex]; } + NSMutableArray *temporaryViews = [NSMutableArray new]; + // Handle read receipts and read marker display. // Ignore the read receipts on the bubble without actual display. // Ignore the read receipts on collapsed bubbles - if ((self.showBubbleReceipts && cellData.readReceipts.count && !isCollapsableCellCollapsed) || self.showReadMarker) + if ((((self.showBubbleReceipts && cellData.readReceipts.count) || cellData.reactions.count) && !isCollapsableCellCollapsed) || self.showReadMarker) { // Read receipts container are inserted here on the right side into the content view. // Some vertical whitespaces are added in message text view (see RoomBubbleCellData class) to insert correctly multiple receipts. - NSInteger index = bubbleComponents.count; - CGFloat bottomPositionY = bubbleCell.frame.size.height; - while (index--) + + NSInteger index = 0; + + for (MXKRoomBubbleComponent *component in bubbleComponents) { - MXKRoomBubbleComponent *component = bubbleComponents[index]; + NSString *componentEventId = component.event.eventId; if (component.event.sentState != MXEventSentStateFailed) { + CGFloat bottomPositionY; + + CGRect bubbleComponentFrame = [bubbleCell componentFrameInContentViewForIndex:index]; + + if (CGRectEqualToRect(bubbleComponentFrame, CGRectNull) == NO) + { + bottomPositionY = bubbleComponentFrame.origin.y + bubbleComponentFrame.size.height; + } + else + { + continue; + } + + MXAggregatedReactions* reactions = cellData.reactions[componentEventId]; + + BubbleReactionsView *reactionsView; + + if (reactions && !isCollapsableCellCollapsed) + { + BOOL showAllReactions = [cellData showAllReactionsForEvent:componentEventId]; + BubbleReactionsViewModel *bubbleReactionsViewModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:reactions + eventId:componentEventId + showAll:showAllReactions]; + + reactionsView = [BubbleReactionsView new]; + reactionsView.viewModel = bubbleReactionsViewModel; + [reactionsView updateWithTheme:ThemeService.shared.theme]; + + [temporaryViews addObject:reactionsView]; + + bubbleReactionsViewModel.viewModelDelegate = self; + + reactionsView.translatesAutoresizingMaskIntoConstraints = NO; + [bubbleCell.contentView addSubview:reactionsView]; + + if (!bubbleCell.tmpSubviews) + { + bubbleCell.tmpSubviews = [NSMutableArray array]; + } + [bubbleCell.tmpSubviews addObject:reactionsView]; + + CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; + + if (self.room.summary.isEncrypted) + { + leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin; + } + + // Force receipts container size + [NSLayoutConstraint activateConstraints: + @[ + [reactionsView.leadingAnchor constraintEqualToAnchor:reactionsView.superview.leadingAnchor constant:leftMargin], + [reactionsView.trailingAnchor constraintEqualToAnchor:reactionsView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin], + [reactionsView.topAnchor constraintEqualToAnchor:reactionsView.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.reactionsViewTopMargin] + ]]; + } + + MXKReceiptSendersContainer* avatarsContainer; + // Handle read receipts (if any) if (self.showBubbleReceipts && cellData.readReceipts.count && !isCollapsableCellCollapsed) { @@ -305,7 +314,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N if (roomMembers.count) { // Define the read receipts container, positioned on the right border of the bubble cell (Note the right margin 6 pts). - MXKReceiptSendersContainer* avatarsContainer = [[MXKReceiptSendersContainer alloc] initWithFrame:CGRectMake(bubbleCell.frame.size.width - 156, bottomPositionY - 13, 150, 12) andMediaManager:self.mxSession.mediaManager]; + avatarsContainer = [[MXKReceiptSendersContainer alloc] initWithFrame:CGRectMake(bubbleCell.frame.size.width - RoomBubbleCellLayout.readReceiptsViewWidth + RoomBubbleCellLayout.readReceiptsViewRightMargin, bottomPositionY + RoomBubbleCellLayout.readReceiptsViewTopMargin, RoomBubbleCellLayout.readReceiptsViewWidth, RoomBubbleCellLayout.readReceiptsViewHeight) andMediaManager:self.mxSession.mediaManager]; // Custom avatar display avatarsContainer.maxDisplayedAvatars = 5; @@ -327,6 +336,8 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N avatarsContainer.translatesAutoresizingMaskIntoConstraints = NO; avatarsContainer.accessibilityIdentifier = @"readReceiptsContainer"; + [temporaryViews addObject:avatarsContainer]; + // Add this read receipts container in the content view if (!bubbleCell.tmpSubviews) { @@ -345,14 +356,14 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 - constant:150]; + constant:RoomBubbleCellLayout.readReceiptsViewWidth]; NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 - constant:12]; + constant:RoomBubbleCellLayout.readReceiptsViewHeight]; // Force receipts container position NSLayoutConstraint *trailingConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer @@ -361,14 +372,19 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N toItem:avatarsContainer.superview attribute:NSLayoutAttributeTrailing multiplier:1.0 - constant:-6]; - NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:avatarsContainer - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:avatarsContainer.superview - attribute:NSLayoutAttributeTop - multiplier:1.0 - constant:bottomPositionY - 13]; + constant:-RoomBubbleCellLayout.readReceiptsViewRightMargin]; + + // At the bottom, we have reactions or nothing + NSLayoutConstraint *topConstraint; + if (reactionsView) + { + topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:reactionsView.bottomAnchor constant:RoomBubbleCellLayout.readReceiptsViewTopMargin]; + } + else + { + topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:avatarsContainer.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.readReceiptsViewTopMargin]; + } + // Available on iOS 8 and later [NSLayoutConstraint activateConstraints:@[widthConstraint, heightConstraint, topConstraint, trailingConstraint]]; @@ -385,9 +401,9 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N bubbleCell.bubbleOverlayContainer.userInteractionEnabled = NO; bubbleCell.bubbleOverlayContainer.hidden = NO; - if ([component.event.eventId isEqualToString:self.room.accountData.readMarkerEventId]) + if ([componentEventId isEqualToString:self.room.accountData.readMarkerEventId]) { - bubbleCell.readMarkerView = [[UIView alloc] initWithFrame:CGRectMake(0, bottomPositionY - 2, bubbleCell.bubbleOverlayContainer.frame.size.width, 2)]; + bubbleCell.readMarkerView = [[UIView alloc] initWithFrame:CGRectMake(0, bottomPositionY - RoomBubbleCellLayout.readMarkerViewHeight, bubbleCell.bubbleOverlayContainer.frame.size.width, RoomBubbleCellLayout.readMarkerViewHeight)]; bubbleCell.readMarkerView.backgroundColor = ThemeService.shared.theme.tintColor; // Hide by default the marker, it will be shown and animated when the cell will be rendered. bubbleCell.readMarkerView.hidden = YES; @@ -399,45 +415,58 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N // Force read marker constraints bubbleCell.readMarkerViewTopConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.readMarkerView - attribute:NSLayoutAttributeTop - relatedBy:NSLayoutRelationEqual - toItem:bubbleCell.bubbleOverlayContainer - attribute:NSLayoutAttributeTop - multiplier:1.0 - constant:bottomPositionY - 2]; - bubbleCell.readMarkerViewLeadingConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.readMarkerView - attribute:NSLayoutAttributeLeading - relatedBy:NSLayoutRelationEqual - toItem:bubbleCell.bubbleOverlayContainer - attribute:NSLayoutAttributeLeading - multiplier:1.0 - constant:0]; - bubbleCell.readMarkerViewTrailingConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.bubbleOverlayContainer - attribute:NSLayoutAttributeTrailing + attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual - toItem:bubbleCell.readMarkerView - attribute:NSLayoutAttributeTrailing + toItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeTop multiplier:1.0 - constant:0]; + constant:bottomPositionY - RoomBubbleCellLayout.readMarkerViewHeight]; + bubbleCell.readMarkerViewLeadingConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.readMarkerView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:0]; + bubbleCell.readMarkerViewTrailingConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.bubbleOverlayContainer + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:bubbleCell.readMarkerView + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:0]; bubbleCell.readMarkerViewHeightConstraint = [NSLayoutConstraint constraintWithItem:bubbleCell.readMarkerView - attribute:NSLayoutAttributeHeight - relatedBy:NSLayoutRelationEqual - toItem:nil - attribute:NSLayoutAttributeNotAnAttribute - multiplier:1.0 - constant:2]; + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:RoomBubbleCellLayout.readMarkerViewHeight]; [NSLayoutConstraint activateConstraints:@[bubbleCell.readMarkerViewTopConstraint, bubbleCell.readMarkerViewLeadingConstraint, bubbleCell.readMarkerViewTrailingConstraint, bubbleCell.readMarkerViewHeightConstraint]]; } } } - // Prepare the bottom position for the next read receipt container (if any) - bottomPositionY = bubbleCell.msgTextViewTopConstraint.constant + component.position.y; + index++; } } + // Update attachmentView bottom constraint to display reactions and read receipts if needed + + UIView *attachmentView = bubbleCell.attachmentView; + NSLayoutConstraint *attachmentViewBottomConstraint = bubbleCell.attachViewBottomConstraint; + + if (attachmentView && temporaryViews.count) + { + attachmentViewBottomConstraint.constant = roomBubbleCellData.additionalContentHeight; + } + else if (attachmentView) + { + [bubbleCell resetAttachmentViewBottomConstraintConstant]; + } + // Check whether an event is currently selected: the other messages are then blurred if (_selectedEventId) { @@ -445,7 +474,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N NSInteger selectedComponentIndex = cellData.selectedComponentIndex; if (selectedComponentIndex != NSNotFound) { - [bubbleCell selectComponent:cellData.selectedComponentIndex]; + [bubbleCell selectComponent:cellData.selectedComponentIndex showEditButton:NO showTimestamp:cellData.showTimestampForSelectedComponent]; } else { @@ -492,11 +521,14 @@ - (void)setSelectedEventId:(NSString *)selectedEventId { RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:_selectedEventId]; cellData.selectedEventId = nil; + cellData.showTimestampForSelectedComponent = NO; } if (selectedEventId.length) { RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:selectedEventId]; + + cellData.showTimestampForSelectedComponent = self.showBubbleDateTimeOnSelection; if (cellData.collapsed && cellData.nextCollapsableCellData) { @@ -523,4 +555,56 @@ - (Widget *)jitsiWidget return jitsiWidget; } +- (void)sendVideo:(NSURL*)videoLocalURL + success:(void (^)(NSString *eventId))success + failure:(void (^)(NSError *error))failure +{ + UIImage *videoThumbnail = [MXKVideoThumbnailGenerator.shared generateThumbnailFrom:videoLocalURL]; + [self sendVideo:videoLocalURL withThumbnail:videoThumbnail success:success failure:failure]; +} + +#pragma mark - BubbleReactionsViewModelDelegate + +- (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didAddReaction:(MXReactionCount *)reactionCount forEventId:(NSString *)eventId +{ + [self addReaction:reactionCount.reaction forEventId:eventId success:^{ + + } failure:^(NSError *error) { + + }]; +} + +- (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didRemoveReaction:(MXReactionCount * _Nonnull)reactionCount forEventId:(NSString * _Nonnull)eventId +{ + [self removeReaction:reactionCount.reaction forEventId:eventId success:^{ + + } failure:^(NSError *error) { + + }]; +} + +- (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didShowAllTappedForEventId:(NSString * _Nonnull)eventId +{ + [self setShowAllReactions:YES forEvent:eventId]; +} + +- (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didShowLessTappedForEventId:(NSString * _Nonnull)eventId +{ + [self setShowAllReactions:NO forEvent:eventId]; +} + +- (void)setShowAllReactions:(BOOL)showAllReactions forEvent:(NSString*)eventId +{ + id cellData = [self cellDataOfEventWithEventId:eventId]; + if ([cellData isKindOfClass:[RoomBubbleCellData class]]) + { + RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)cellData; + + [roomBubbleCellData setShowAllReactions:showAllReactions forEvent:eventId]; + [self updateCellDataReactions:roomBubbleCellData forEventId:eventId]; + + [self.delegate dataSource:self didCellChange:nil]; + } +} + @end diff --git a/Riot/Modules/Room/EditHistory/EditHistoryCell.swift b/Riot/Modules/Room/EditHistory/EditHistoryCell.swift new file mode 100644 index 0000000000..0d05ef6c2c --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryCell.swift @@ -0,0 +1,38 @@ +/* + 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 EditHistoryCell: UITableViewCell, NibReusable, Themable { + + // MARK: - Properties + + @IBOutlet private weak var timestampLabel: UILabel! + @IBOutlet private weak var messageLabel: UILabel! + + // MARK: - Public + + func fill(with timeString: String, and attributedMessage: NSAttributedString) { + self.timestampLabel.text = timeString + self.messageLabel.attributedText = attributedMessage + } + + func update(theme: Theme) { + self.backgroundColor = theme.backgroundColor + self.timestampLabel.textColor = theme.textSecondaryColor + } +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryCell.xib b/Riot/Modules/Room/EditHistory/EditHistoryCell.xib new file mode 100644 index 0000000000..7780255db8 --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryCell.xib @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/EditHistory/EditHistoryCoordinator.swift b/Riot/Modules/Room/EditHistory/EditHistoryCoordinator.swift new file mode 100644 index 0000000000..7cabf0a738 --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryCoordinator.swift @@ -0,0 +1,67 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/EditHistory EditHistory +/* + 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 EditHistoryCoordinator: EditHistoryCoordinatorType { + + // MARK: - Properties + + // MARK: Private + + private var editHistoryViewModel: EditHistoryViewModelType + private let editHistoryViewController: EditHistoryViewController + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: EditHistoryCoordinatorDelegate? + + // MARK: - Setup + + init(session: MXSession, + formatter: MXKEventFormatter, + event: MXEvent) { + + let editHistoryViewModel = EditHistoryViewModel(session: session, formatter: formatter, event: event) + let editHistoryViewController = EditHistoryViewController.instantiate(with: editHistoryViewModel) + self.editHistoryViewModel = editHistoryViewModel + self.editHistoryViewController = editHistoryViewController + } + + // MARK: - Public methods + + func start() { + self.editHistoryViewModel.coordinatorDelegate = self + } + + func toPresentable() -> UIViewController { + return self.editHistoryViewController + } +} + +// MARK: - EditHistoryViewModelCoordinatorDelegate +extension EditHistoryCoordinator: EditHistoryViewModelCoordinatorDelegate { + + func editHistoryViewModelDidClose(_ viewModel: EditHistoryViewModelType) { + self.delegate?.editHistoryCoordinatorDidComplete(self) + } +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryCoordinatorBridgePresenter.swift b/Riot/Modules/Room/EditHistory/EditHistoryCoordinatorBridgePresenter.swift new file mode 100644 index 0000000000..f6b20b5e54 --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryCoordinatorBridgePresenter.swift @@ -0,0 +1,118 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Room/EditHistory EditHistory +/* + 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 EditHistoryCoordinatorBridgePresenterDelegate { + func editHistoryCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: EditHistoryCoordinatorBridgePresenter) +} + +/// EditHistoryCoordinatorBridgePresenter enables to start EditHistoryCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +@objcMembers +final class EditHistoryCoordinatorBridgePresenter: NSObject { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private let event: MXEvent + private var coordinator: EditHistoryCoordinator? + + // MARK: Public + + weak var delegate: EditHistoryCoordinatorBridgePresenterDelegate? + + // MARK: - Setup + + init(session: MXSession, + event: MXEvent) { + self.session = session + self.event = event + super.init() + } + + // MARK: - Public + + // NOTE: Default value feature is not compatible with Objective-C. + // func present(from viewController: UIViewController, animated: Bool) { + // self.present(from: viewController, animated: animated) + // } + + func present(from viewController: UIViewController, animated: Bool) { + + guard let formatter = self.createEventFormatter(session: self.session) else { + //s das + return + } + + let editHistoryCoordinator = EditHistoryCoordinator(session: self.session, formatter: formatter, event: self.event) + editHistoryCoordinator.delegate = self + + let navigationController = RiotNavigationController() + navigationController.modalPresentationStyle = .formSheet + navigationController.addChild(editHistoryCoordinator.toPresentable()) + viewController.present(navigationController, animated: animated, completion: nil) + + editHistoryCoordinator.start() + + self.coordinator = editHistoryCoordinator + } + + 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: - Private + + func createEventFormatter(session: MXSession) -> EventFormatter? { + guard let formatter = EventFormatter(matrixSession: session) else { + print("[EditHistoryCoordinatorBridgePresenter] createEventFormatter: Cannot build formatter") + return nil + } + + // Use the same event formatter settings as RoomDataSource + formatter.treatMatrixUserIdAsLink = true + formatter.treatMatrixRoomIdAsLink = true + formatter.treatMatrixRoomAliasAsLink = true + formatter.treatMatrixGroupIdAsLink = true + formatter.eventTypesFilterForMessages = MXKAppSettings.standard()?.eventsFilterForMessages + + // But do not display "...(Edited)" + formatter.showEditionMention = false + + return formatter + } +} + +// MARK: - EditHistoryCoordinatorDelegate +extension EditHistoryCoordinatorBridgePresenter: EditHistoryCoordinatorDelegate { + func editHistoryCoordinatorDidComplete(_ coordinator: EditHistoryCoordinatorType) { + self.delegate?.editHistoryCoordinatorBridgePresenterDelegateDidComplete(self) + } +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryCoordinatorType.swift b/Riot/Modules/Room/EditHistory/EditHistoryCoordinatorType.swift new file mode 100644 index 0000000000..2f02bd705f --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryCoordinatorType.swift @@ -0,0 +1,28 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/EditHistory EditHistory +/* + 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 EditHistoryCoordinatorDelegate: class { + func editHistoryCoordinatorDidComplete(_ coordinator: EditHistoryCoordinatorType) +} + +/// `EditHistoryCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. +protocol EditHistoryCoordinatorType: Coordinator, Presentable { + var delegate: EditHistoryCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryHeaderView.swift b/Riot/Modules/Room/EditHistory/EditHistoryHeaderView.swift new file mode 100644 index 0000000000..a547024d1e --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryHeaderView.swift @@ -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 EditHistoryHeaderView: UITableViewHeaderFooterView, NibLoadable, Reusable, Themable { + + // MARK: - Properties + + @IBOutlet private weak var dateLabel: UILabel! + + // MARK: - Public + + func update(theme: Theme) { + self.contentView.backgroundColor = theme.backgroundColor + self.dateLabel.textColor = theme.headerTextPrimaryColor + } + + func fill(with dateString: String) { + self.dateLabel.text = dateString + } +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryHeaderView.xib b/Riot/Modules/Room/EditHistory/EditHistoryHeaderView.xib new file mode 100644 index 0000000000..e0a28bcf58 --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryHeaderView.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/EditHistory/EditHistoryMessage.swift b/Riot/Modules/Room/EditHistory/EditHistoryMessage.swift new file mode 100644 index 0000000000..f338649602 --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryMessage.swift @@ -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 + +struct EditHistoryMessage { + let date: Date + let message: NSAttributedString +} diff --git a/Riot/Modules/Room/EditHistory/EditHistorySection.swift b/Riot/Modules/Room/EditHistory/EditHistorySection.swift new file mode 100644 index 0000000000..550378541b --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistorySection.swift @@ -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 + +struct EditHistorySection { + let date: Date + let messages: [EditHistoryMessage] +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryViewAction.swift b/Riot/Modules/Room/EditHistory/EditHistoryViewAction.swift new file mode 100644 index 0000000000..d1fed7fc51 --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryViewAction.swift @@ -0,0 +1,25 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/EditHistory EditHistory +/* + 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 + +/// EditHistoryViewController view actions exposed to view model +enum EditHistoryViewAction { + case loadMore + case close +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryViewController.storyboard b/Riot/Modules/Room/EditHistory/EditHistoryViewController.storyboard new file mode 100644 index 0000000000..059dae3098 --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryViewController.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/EditHistory/EditHistoryViewController.swift b/Riot/Modules/Room/EditHistory/EditHistoryViewController.swift new file mode 100644 index 0000000000..88c254182b --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryViewController.swift @@ -0,0 +1,228 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/EditHistory EditHistory +/* + 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 EditHistoryViewController: UIViewController { + + // MARK: - Constants + + private enum Constants { + static let estimatedRowHeight: CGFloat = 38.0 + static let estimatedSectionHeaderHeight: CGFloat = 28.0 + static let editHistoryMessageTimeFormat = "HH:mm" + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var tableView: UITableView! + + // MARK: Private + + private var viewModel: EditHistoryViewModelType! + private var theme: Theme! + private var errorPresenter: MXKErrorPresentation! + private var activityIndicatorPresenter: ActivityIndicatorPresenter! + + private var editHistorySections: [EditHistorySection] = [] + + private lazy var sectionDateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .full + dateFormatter.timeStyle = .none + dateFormatter.doesRelativeDateFormatting = true + return dateFormatter + }() + + private lazy var messageDateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = Constants.editHistoryMessageTimeFormat + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + return dateFormatter + }() + + // MARK: - Setup + + class func instantiate(with viewModel: EditHistoryViewModelType) -> EditHistoryViewController { + let viewController = StoryboardScene.EditHistoryViewController.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.roomMessageEditsHistoryTitle + + self.setupViews() + self.activityIndicatorPresenter = ActivityIndicatorPresenter() + self.errorPresenter = MXKErrorAlertPresentation() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + + self.viewModel.viewDelegate = self + + self.viewModel.process(viewAction: .loadMore) + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return self.theme.statusBarStyle + } + + // MARK: - Private + + private func update(theme: Theme) { + self.theme = theme + + self.view.backgroundColor = theme.backgroundColor + 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 setupViews() { + let closeBarButtonItem = MXKBarButtonItem(title: VectorL10n.close, style: .plain) { [weak self] in + self?.closeButtonAction() + } + + self.navigationItem.rightBarButtonItem = closeBarButtonItem + + self.setupTableView() + } + + private func setupTableView() { + self.tableView.rowHeight = UITableView.automaticDimension + self.tableView.estimatedRowHeight = Constants.estimatedRowHeight + self.tableView.register(cellType: EditHistoryCell.self) + + self.tableView.sectionHeaderHeight = UITableView.automaticDimension + self.tableView.estimatedSectionHeaderHeight = Constants.estimatedSectionHeaderHeight + self.tableView.register(headerFooterViewType: EditHistoryHeaderView.self) + + self.tableView.tableFooterView = UIView() + } + + private func render(viewState: EditHistoryViewState) { + switch viewState { + case .loading: + self.renderLoading() + case .loaded(let sections, let addedCount, let allDataLoaded): + self.renderLoaded(sections: sections, addedCount: addedCount, allDataLoaded: allDataLoaded) + case .error(let error): + self.render(error: error) + } + } + + private func renderLoading() { + self.activityIndicatorPresenter.presentActivityIndicator(on: self.view, animated: true) + } + + private func renderLoaded(sections: [EditHistorySection], addedCount: Int, allDataLoaded: Bool) { + self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + self.editHistorySections = sections + self.tableView.reloadData() + } + + private func render(error: Error) { + self.activityIndicatorPresenter.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 EditHistoryViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return self.editHistorySections.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.editHistorySections[section].messages.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let editHistoryCell = tableView.dequeueReusableCell(for: indexPath, cellType: EditHistoryCell.self) + + let editHistoryMessage = self.editHistorySections[indexPath.section].messages[indexPath.row] + + let timeString = self.messageDateFormatter.string(from: editHistoryMessage.date) + + editHistoryCell.update(theme: self.theme) + editHistoryCell.fill(with: timeString, and: editHistoryMessage.message) + + return editHistoryCell + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let editHistoryHeaderView: EditHistoryHeaderView = tableView.dequeueReusableHeaderFooterView() else { + return nil + } + let editHistorySection = self.editHistorySections[section] + let dateString = self.sectionDateFormatter.string(from: editHistorySection.date) + + editHistoryHeaderView.update(theme: self.theme) + editHistoryHeaderView.fill(with: dateString) + return editHistoryHeaderView + } +} + +// MARK: - UITableViewDelegate +extension EditHistoryViewController: UITableViewDelegate { + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + + // 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: - EditHistoryViewModelViewDelegate +extension EditHistoryViewController: EditHistoryViewModelViewDelegate { + + func editHistoryViewModel(_ viewModel: EditHistoryViewModelType, didUpdateViewState viewSate: EditHistoryViewState) { + self.render(viewState: viewSate) + } +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryViewModel.swift b/Riot/Modules/Room/EditHistory/EditHistoryViewModel.swift new file mode 100644 index 0000000000..461f32699a --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryViewModel.swift @@ -0,0 +1,225 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/EditHistory EditHistory +/* + 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 EditHistoryViewModel: EditHistoryViewModelType { + + // MARK: - Constants + + private enum Pagination { + static let count: UInt = 30 + } + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private let aggregations: MXAggregations + private let formatter: MXKEventFormatter + private let roomId: String + private let event: MXEvent + private let messageFormattingQueue: DispatchQueue + + private var messages: [EditHistoryMessage] = [] + private var operation: MXHTTPOperation? + private var nextBatch: String? + private var viewState: EditHistoryViewState? + + // MARK: Public + + weak var viewDelegate: EditHistoryViewModelViewDelegate? + weak var coordinatorDelegate: EditHistoryViewModelCoordinatorDelegate? + + // MARK: - Setup + + init(session: MXSession, + formatter: MXKEventFormatter, + event: MXEvent) { + self.session = session + self.aggregations = session.aggregations + self.formatter = formatter + self.event = event + self.roomId = event.roomId + self.messageFormattingQueue = DispatchQueue(label: "\(type(of: self)).messageFormattingQueue") + } + + // MARK: - Public + + func process(viewAction: EditHistoryViewAction) { + switch viewAction { + case .loadMore: + self.loadMoreHistory() + case .close: + self.coordinatorDelegate?.editHistoryViewModelDidClose(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(sections: _, addedCount: _, allDataLoaded: let allLoaded): + canLoadMoreHistory = !allLoaded + default: + canLoadMoreHistory = true + } + + return canLoadMoreHistory + } + + private func loadMoreHistory() { + guard self.canLoadMoreHistory() else { + print("[EditHistoryViewModel] loadMoreHistory: pending loading or all data loaded") + return + } + + guard self.operation == nil else { + print("[EditHistoryViewModel] loadMoreHistory: operation already pending") + return + } + + self.update(viewState: .loading) + + self.operation = self.aggregations.replaceEvents(forEvent: self.event.eventId, isEncrypted: self.event.isEncrypted, inRoom: self.roomId, from: self.nextBatch, limit: Pagination.count, success: { [weak self] (response) in + guard let sself = self else { + return + } + + sself.nextBatch = response.nextBatch + sself.operation = nil + + sself.process(editEvents: response.chunk) + + if response.nextBatch == nil { + // Append the original event when hitting the end of the edits history + if let originalEvent = response.originalEvent { + sself.process(editEvents: [originalEvent]) + } else { + print("[EditHistoryViewModel] loadMoreHistory: The homeserver did not return the original event") + } + } + + }, failure: { [weak self] error in + guard let sself = self else { + return + } + + sself.operation = nil + sself.update(viewState: .error(error)) + }) + } + + private func process(editEvents: [MXEvent]) { + self.messageFormattingQueue.async { + + let newMessages = editEvents.reversed() + .compactMap { (editEvent) -> EditHistoryMessage? in + return self.process(editEvent: editEvent) + } + + let allDataLoaded = self.nextBatch == nil + let addedCount: Int + + if newMessages.count > 0 { + self.messages.append(contentsOf: newMessages) + addedCount = newMessages.count + } else { + addedCount = 0 + } + + let editHistorySections = self.editHistorySections(from: self.messages) + + DispatchQueue.main.async { + self.update(viewState: .loaded(sections: editHistorySections, addedCount: addedCount, allDataLoaded: allDataLoaded)) + } + } + } + + private func editHistorySections(from editHistoryMessages: [EditHistoryMessage]) -> [EditHistorySection] { + + // Group edit messages by day + + let initial: [Date: [EditHistoryMessage]] = [:] + let dateComponents: Set = [.day, .month, .year] + let calendar = Calendar.current + + let messagesGroupedByDay = editHistoryMessages.reduce(into: initial) { messagesByDay, message in + let components = calendar.dateComponents(dateComponents, from: message.date) + if let date = calendar.date(from: components) { + var messages = messagesByDay[date] ?? [] + messages.append(message) + messagesByDay[date] = messages + } + } + + // Create edit sections + + var sections: [EditHistorySection] = [] + + for (date, messages) in messagesGroupedByDay { + // Sort messages descending (most recent first) + let sortedMessages = messages.sorted { $0.date.compare($1.date) == .orderedDescending } + let section = EditHistorySection(date: date, messages: sortedMessages) + sections.append(section) + } + + // Sort sections descending (most recent first) + let sortedSections = sections.sorted { $0.date.compare($1.date) == .orderedDescending } + + return sortedSections + } + + private func process(editEvent: MXEvent) -> EditHistoryMessage? { + // Create a temporary MXEvent that represents this edition + guard let editedEvent = self.event.editedEvent(fromReplacementEvent: editEvent) else { + print("[EditHistoryViewModel] processEditEvent: Cannot build edited event: \(editEvent.eventId ?? "")") + return nil + } + + if editedEvent.isEncrypted && editedEvent.clear == nil { + if self.session.decryptEvent(editedEvent, inTimeline: nil) == false { + print("[EditHistoryViewModel] processEditEvent: Fail to decrypt event: \(editedEvent.eventId ?? "")") + } + } + + let formatterError = UnsafeMutablePointer.allocate(capacity: 1) + guard let message = self.formatter.attributedString(from: editedEvent, with: nil, error: formatterError) else { + print("[EditHistoryViewModel] processEditEvent: cannot format(error: \(formatterError)) edited event: \(editedEvent.eventId ?? "")") + return nil + } + + let date = Date(timeIntervalSince1970: TimeInterval(editEvent.originServerTs) / 1000) + + return EditHistoryMessage(date: date, message: message) + } + + private func update(viewState: EditHistoryViewState) { + self.viewState = viewState + self.viewDelegate?.editHistoryViewModel(self, didUpdateViewState: viewState) + } +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryViewModelType.swift b/Riot/Modules/Room/EditHistory/EditHistoryViewModelType.swift new file mode 100644 index 0000000000..4ed9ff7078 --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryViewModelType.swift @@ -0,0 +1,36 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/EditHistory EditHistory +/* + 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 EditHistoryViewModelViewDelegate: class { + func editHistoryViewModel(_ viewModel: EditHistoryViewModelType, didUpdateViewState viewSate: EditHistoryViewState) +} + +protocol EditHistoryViewModelCoordinatorDelegate: class { + func editHistoryViewModelDidClose(_ viewModel: EditHistoryViewModelType) +} + +/// Protocol describing the view model used by `EditHistoryViewController` +protocol EditHistoryViewModelType { + + var viewDelegate: EditHistoryViewModelViewDelegate? { get set } + var coordinatorDelegate: EditHistoryViewModelCoordinatorDelegate? { get set } + + func process(viewAction: EditHistoryViewAction) +} diff --git a/Riot/Modules/Room/EditHistory/EditHistoryViewState.swift b/Riot/Modules/Room/EditHistory/EditHistoryViewState.swift new file mode 100644 index 0000000000..bb48192ee1 --- /dev/null +++ b/Riot/Modules/Room/EditHistory/EditHistoryViewState.swift @@ -0,0 +1,26 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room/EditHistory EditHistory +/* + 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 + +/// EditHistoryViewController view state +enum EditHistoryViewState { + case loading + case loaded(sections: [EditHistorySection], addedCount: Int, allDataLoaded: Bool) + case error(Error) +} diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 49440b8da7..5b574c56f8 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -123,7 +123,8 @@ #import "Riot-Swift.h" -@interface RoomViewController () +@interface RoomViewController () { // The expanded header ExpandedRoomTitleView *expandedHeader; @@ -206,9 +207,6 @@ @interface RoomViewController () )cell withTappedEvent:(MXEvent*)event +{ + if (event && !customizedRoomDataSource.selectedEventId) + { + [self showContextualMenuForEvent:event fromSingleTapGesture:NO cell:cell animated:YES]; + } +} + #pragma mark - Hide/Show expanded header - (void)showExpandedHeader:(BOOL)isVisible @@ -1545,6 +1597,9 @@ - (void)showExpandedHeader:(BOOL)isVisible mainNavigationController.navigationBar.translucent = isVisible; self.navigationController.navigationBar.translucent = isVisible; + // Hide contextual menu if needed + [self hideContextualMenuAnimated:YES]; + [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{ @@ -1976,6 +2031,15 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac // Handle here user actions on bubbles for Vector app if (customizedRoomDataSource) { + id bubbleData; + + if ([cell isKindOfClass:[MXKRoomBubbleTableViewCell class]]) + { + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell; + bubbleData = roomBubbleTableViewCell.bubbleData; + } + + if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAvatarView]) { selectedRoomMember = [self.roomDataSource.roomState.members memberWithUserId:userInfo[kMXKRoomBubbleCellUserIdKey]]; @@ -2019,13 +2083,17 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac } else { - // Highlight this event in displayed message - [self selectEventWithId:tappedEvent.eventId]; + // Show contextual menu on single tap if bubble is not collapsed + if (bubbleData.collapsed) + { + [self selectEventWithId:tappedEvent.eventId]; + } + else + { + [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES]; + } } } - - // Force table refresh - [self dataSource:self.roomDataSource didCellChange:nil]; } else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnOverlayContainer]) { @@ -2040,7 +2108,7 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac if (selectedEvent) { - [self showEditButtonAlertMenuForEvent:selectedEvent inCell:cell level:0]; + [self showContextualMenuForEvent:selectedEvent fromSingleTapGesture:YES cell:cell animated:YES]; } } else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAttachmentView]) @@ -2066,9 +2134,6 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac // Highlight this event in displayed message [self selectEventWithId:((MXKRoomBubbleTableViewCell*)cell).bubbleData.attachment.eventId]; } - - // Force table refresh - [self dataSource:self.roomDataSource didCellChange:nil]; } else { @@ -2098,6 +2163,15 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac [self.roomDataSource collapseRoomBubble:((MXKRoomBubbleTableViewCell*)cell).bubbleData collapsed:YES]; } + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnEvent]) + { + MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey]; + + if (!bubbleData.collapsed) + { + [self handleLongPressFromCell:cell withTappedEvent:tappedEvent]; + } + } else { // Keep default implementation for other actions @@ -2111,8 +2185,8 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac } } -// Display the edit menu on 2 pages/levels. -- (void)showEditButtonAlertMenuForEvent:(MXEvent*)selectedEvent inCell:(id)cell level:(NSUInteger)level; +// Display the additiontal event actions menu +- (void)showAdditionalActionsMenuForEvent:(MXEvent*)selectedEvent inCell:(id)cell animated:(BOOL)animated { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment; @@ -2126,42 +2200,39 @@ - (void)showEditButtonAlertMenuForEvent:(MXEvent*)selectedEvent inCell:(idcurrentAlert = nil; - - // Cancel and remove the outgoing message - [self.roomDataSource.room cancelSendingOperation:selectedEvent.eventId]; - [self.roomDataSource removeEventWithEventId:selectedEvent.eventId]; - - [self cancelEventSelection]; - } - - }]]; - } - } - if (level == 0) + // Check status of the selected event + if (selectedEvent.sentState == MXEventSentStatePreparing || + selectedEvent.sentState == MXEventSentStateEncrypting || + selectedEvent.sentState == MXEventSentStateSending) { - [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_copy", @"Vector", nil) + [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_cancel_send", @"Vector", nil) style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + handler:^(UIAlertAction * action) + { + if (weakSelf) + { + typeof(self) self = weakSelf; + + self->currentAlert = nil; + + // Cancel and remove the outgoing message + [self.roomDataSource.room cancelSendingOperation:selectedEvent.eventId]; + [self.roomDataSource removeEventWithEventId:selectedEvent.eventId]; + + [self cancelEventSelection]; + } + + }]]; + } + + [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_quote", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; - if (weakSelf) - { - typeof(self) self = weakSelf; - - [self cancelEventSelection]; - - [[UIPasteboard generalPasteboard] setString:selectedComponent.textMessage]; - } + [self cancelEventSelection]; - }]]; - } - - if (level == 0) - { - [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_quote", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + // Quote the message a la Markdown into the input toolbar composer + self.inputToolbarView.textMessage = [NSString stringWithFormat:@"%@\n>%@\n\n", self.inputToolbarView.textMessage, selectedComponent.textMessage]; - if (weakSelf) + // And display the keyboard + [self.inputToolbarView becomeFirstResponder]; + } + + }]]; + + [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_share", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + + [self cancelEventSelection]; + + NSArray *activityItems = @[selectedComponent.textMessage]; + + UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil]; + + if (activityViewController) { - typeof(self) self = weakSelf; - - [self cancelEventSelection]; + activityViewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical; + activityViewController.popoverPresentationController.sourceView = roomBubbleTableViewCell; + activityViewController.popoverPresentationController.sourceRect = roomBubbleTableViewCell.bounds; - // Quote the message a la Markdown into the input toolbar composer - self.inputToolbarView.textMessage = [NSString stringWithFormat:@"%@\n>%@\n\n", self.inputToolbarView.textMessage, selectedComponent.textMessage]; - - // And display the keyboard - [self.inputToolbarView becomeFirstResponder]; + [self presentViewController:activityViewController animated:YES completion:nil]; } - - }]]; - } - - if (level == 1) + } + + }]]; + } + else // Add action for attachment + { + if (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo) { - [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_share", @"Vector", nil) + [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_save", @"Vector", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { @@ -2259,145 +2335,75 @@ - (void)showEditButtonAlertMenuForEvent:(MXEvent*)selectedEvent inCell:(idcurrentAlert = nil; - [self startActivityIndicator]; + // Remove the outgoing message and its related cached file. + [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.cacheFilePath error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.thumbnailCachePath error:nil]; - [attachment copy:^{ - - __strong __typeof(weakSelf)self = weakSelf; - [self stopActivityIndicator]; - - } failure:^(NSError *error) { - - __strong __typeof(weakSelf)self = weakSelf; - [self stopActivityIndicator]; - - //Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; + // Cancel and remove the outgoing message + [self.roomDataSource.room cancelSendingOperation:selectedEvent.eventId]; + [self.roomDataSource removeEventWithEventId:selectedEvent.eventId]; - // Start animation in case of download during attachment preparing - [roomBubbleTableViewCell startProgressUI]; + [self cancelEventSelection]; } }]]; } - - // Check status of the selected event - if (selectedEvent.sentState == MXEventSentStatePreparing || - selectedEvent.sentState == MXEventSentStateEncrypting || - selectedEvent.sentState == MXEventSentStateUploading || - selectedEvent.sentState == MXEventSentStateSending) - { - // Upload id is stored in attachment url (nasty trick) - NSString *uploadId = roomBubbleTableViewCell.bubbleData.attachment.contentURL; - if ([MXMediaManager existingUploaderWithId:uploadId]) - { - [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_cancel_send", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - // Get again the loader - MXMediaLoader *loader = [MXMediaManager existingUploaderWithId:uploadId]; - if (loader) - { - [loader cancel]; - } - // Hide the progress animation - roomBubbleTableViewCell.progressView.hidden = YES; - - if (weakSelf) - { - typeof(self) self = weakSelf; - - self->currentAlert = nil; - - // Remove the outgoing message and its related cached file. - [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.cacheFilePath error:nil]; - [[NSFileManager defaultManager] removeItemAtPath:roomBubbleTableViewCell.bubbleData.attachment.thumbnailCachePath error:nil]; - - // Cancel and remove the outgoing message - [self.roomDataSource.room cancelSendingOperation:selectedEvent.eventId]; - [self.roomDataSource removeEventWithEventId:selectedEvent.eventId]; - - [self cancelEventSelection]; - } - - }]]; - } - } } - - if (level == 1 && (attachment.type != MXKAttachmentTypeSticker)) + + if (attachment.type != MXKAttachmentTypeSticker) { [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_share", @"Vector", nil) style:UIAlertActionStyleDefault @@ -2442,7 +2448,7 @@ - (void)showEditButtonAlertMenuForEvent:(MXEvent*)selectedEvent inCell:(idcurrentAlert = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"room_event_action_report_prompt_reason", @"Vector", nil) message:nil preferredStyle:UIAlertControllerStyleAlert]; - - [self->currentAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) { - textField.secureTextEntry = NO; - textField.placeholder = nil; - textField.keyboardType = UIKeyboardTypeDefault; - }]; + [self cancelEventSelection]; + + // Prompt user to enter a description of the problem content. + self->currentAlert = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"room_event_action_report_prompt_reason", @"Vector", nil) message:nil preferredStyle:UIAlertControllerStyleAlert]; + + [self->currentAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.secureTextEntry = NO; + textField.placeholder = nil; + textField.keyboardType = UIKeyboardTypeDefault; + }]; + + [self->currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - [self->currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + if (weakSelf) + { + typeof(self) self = weakSelf; + NSString *text = [self->currentAlert textFields].firstObject.text; + self->currentAlert = nil; - if (weakSelf) - { - typeof(self) self = weakSelf; - NSString *text = [self->currentAlert textFields].firstObject.text; - self->currentAlert = nil; + [self startActivityIndicator]; + + [self.roomDataSource.room reportEvent:selectedEvent.eventId score:-100 reason:text success:^{ - [self startActivityIndicator]; + __strong __typeof(weakSelf)self = weakSelf; + [self stopActivityIndicator]; - [self.roomDataSource.room reportEvent:selectedEvent.eventId score:-100 reason:text success:^{ - - __strong __typeof(weakSelf)self = weakSelf; - [self stopActivityIndicator]; - - // Prompt user to ignore content from this user - self->currentAlert = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"room_event_action_report_prompt_ignore_user", @"Vector", nil) message:nil preferredStyle:UIAlertControllerStyleAlert]; + // Prompt user to ignore content from this user + self->currentAlert = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"room_event_action_report_prompt_ignore_user", @"Vector", nil) message:nil preferredStyle:UIAlertControllerStyleAlert]; + + [self->currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"yes"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - [self->currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"yes"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + if (weakSelf) + { + typeof(self) self = weakSelf; + self->currentAlert = nil; + + [self startActivityIndicator]; - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; + // Add the user to the blacklist: ignored users + [self.mainSession ignoreUsers:@[selectedEvent.sender] success:^{ - [self startActivityIndicator]; + __strong __typeof(weakSelf)self = weakSelf; + [self stopActivityIndicator]; - // Add the user to the blacklist: ignored users - [self.mainSession ignoreUsers:@[selectedEvent.sender] success:^{ - - __strong __typeof(weakSelf)self = weakSelf; - [self stopActivityIndicator]; - - } failure:^(NSError *error) { - - __strong __typeof(weakSelf)self = weakSelf; - [self stopActivityIndicator]; - - NSLog(@"[RoomVC] Ignore user (%@) failed", selectedEvent.sender); - //Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; - - }]; - } - - }]]; - - [self->currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"no"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [self presentViewController:self->currentAlert animated:YES completion:nil]; - - } failure:^(NSError *error) { + } failure:^(NSError *error) { + + __strong __typeof(weakSelf)self = weakSelf; + [self stopActivityIndicator]; + + NSLog(@"[RoomVC] Ignore user (%@) failed", selectedEvent.sender); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + + }]; + } - __strong __typeof(weakSelf)self = weakSelf; - [self stopActivityIndicator]; + }]]; + + [self->currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"no"] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - NSLog(@"[RoomVC] Report event (%@) failed", selectedEvent.eventId); - //Alert user - [[AppDelegate theDelegate] showErrorAsAlert:error]; + if (weakSelf) + { + typeof(self) self = weakSelf; + self->currentAlert = nil; + } - }]; - } - - }]]; - - [self->currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; + }]]; + + [self presentViewController:self->currentAlert animated:YES completion:nil]; + + } failure:^(NSError *error) { + + __strong __typeof(weakSelf)self = weakSelf; + [self stopActivityIndicator]; + + NSLog(@"[RoomVC] Report event (%@) failed", selectedEvent.eventId); + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + + }]; + } - [self presentViewController:self->currentAlert animated:YES completion:nil]; - } - - }]]; - } - - if (level == 1 && self.roomDataSource.room.summary.isEncrypted) - { - [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_view_encryption", @"Vector", nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + }]]; - if (weakSelf) - { - typeof(self) self = weakSelf; - [self cancelEventSelection]; + [self->currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { - // Display encryption details - [self showEncryptionInformation:selectedEvent]; - } + if (weakSelf) + { + typeof(self) self = weakSelf; + self->currentAlert = nil; + } + + }]]; - }]]; - } - - - if (level == 0) + [self presentViewController:self->currentAlert animated:YES completion:nil]; + } + + }]]; + + if (self.roomDataSource.room.summary.isEncrypted) { - [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_more", @"Vector", nil) + [currentAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_event_action_view_encryption", @"Vector", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { if (weakSelf) { typeof(self) self = weakSelf; - self->currentAlert = nil; + [self cancelEventSelection]; - // Show the next level of options - [self showEditButtonAlertMenuForEvent:selectedEvent inCell:cell level:1]; + // Display encryption details + [self showEncryptionInformation:selectedEvent]; } }]]; @@ -2726,7 +2702,7 @@ - (void)showEditButtonAlertMenuForEvent:(MXEvent*)selectedEvent inCell:(id 1) { + NSArray *components = roomBubbleTableViewCell.bubbleData.bubbleComponents; + + NSInteger index = 0; + for (MXKRoomBubbleComponent *component in components) + { + if ([component.event.eventId isEqualToString:selectedEvent.eventId]) + { + break; + } + index++; + } + + CGRect sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:index]; + [currentAlert mxk_setAccessibilityIdentifier:@"RoomVCEventMenuAlert"]; [currentAlert popoverPresentationController].sourceView = roomBubbleTableViewCell; - [currentAlert popoverPresentationController].sourceRect = roomBubbleTableViewCell.bounds; - [self presentViewController:currentAlert animated:YES completion:nil]; + [currentAlert popoverPresentationController].sourceRect = sourceRect; + [self presentViewController:currentAlert animated:animated completion:nil]; } else { @@ -2819,9 +2809,9 @@ - (BOOL)dataSource:(MXKDataSource *)dataSource shouldDoAction:(NSString *)action NSString *fragment = [NSString stringWithFormat:@"/group/%@", [MXTools encodeURIComponent:absoluteURLString]]; [[AppDelegate theDelegate] handleUniversalLinkFragment:fragment]; } - else if ([absoluteURLString hasPrefix:kEventFormatterOnReRequestKeysLinkAction]) + else if ([absoluteURLString hasPrefix:EventFormatterOnReRequestKeysLinkAction]) { - NSArray *arguments = [absoluteURLString componentsSeparatedByString:kEventFormatterOnReRequestKeysLinkActionSeparator]; + NSArray *arguments = [absoluteURLString componentsSeparatedByString:EventFormatterLinkActionSeparator]; if (arguments.count > 1) { NSString *eventId = arguments[1]; @@ -2833,13 +2823,21 @@ - (BOOL)dataSource:(MXKDataSource *)dataSource shouldDoAction:(NSString *)action } } } + else if ([absoluteURLString hasPrefix:EventFormatterEditedEventLinkAction]) + { + NSArray *arguments = [absoluteURLString componentsSeparatedByString:EventFormatterLinkActionSeparator]; + if (arguments.count > 1) + { + NSString *eventId = arguments[1]; + [self showEditHistoryForEventId:eventId animated:YES]; + } + shouldDoAction = NO; + } else if (url && urlItemInteractionValue) { // Fallback case for external links - - // TODO: Use UITextItemInteraction enum when minimum deployement target will be iOS 10 switch (urlItemInteractionValue.integerValue) { - case 0: //UITextItemInteractionInvokeDefaultAction + case UITextItemInteractionInvokeDefaultAction: { [[UIApplication sharedApplication] vc_open:url completionHandler:^(BOOL success) { if (!success) @@ -2850,10 +2848,13 @@ - (BOOL)dataSource:(MXKDataSource *)dataSource shouldDoAction:(NSString *)action shouldDoAction = NO; } break; - case 1: //UITextItemInteractionPresentActions - // Long press on link, let MXKRoomBubbleTableViewCell UITextView present the default contextual menu. + case UITextItemInteractionPresentActions: + { + // Long press on link, present room contextual menu. + shouldDoAction = NO; + } break; - case 2: //UITextItemInteractionPreview + case UITextItemInteractionPreview: // Force touch on link, let MXKRoomBubbleTableViewCell UITextView use default peek and pop behavior. break; default: @@ -2871,16 +2872,23 @@ - (BOOL)dataSource:(MXKDataSource *)dataSource shouldDoAction:(NSString *)action - (void)selectEventWithId:(NSString*)eventId { - BOOL shouldEnableReplyMode = [self.roomDataSource canReplyToEventWithId:eventId];; - - [self enableReplyMode:shouldEnableReplyMode]; + [self selectEventWithId:eventId inputToolBarSendMode:RoomInputToolbarViewSendModeSend showTimestamp:YES]; +} + +- (void)selectEventWithId:(NSString*)eventId inputToolBarSendMode:(RoomInputToolbarViewSendMode)inputToolBarSendMode showTimestamp:(BOOL)showTimestamp +{ + [self setInputToolBarSendMode:inputToolBarSendMode]; + customizedRoomDataSource.showBubbleDateTimeOnSelection = showTimestamp; customizedRoomDataSource.selectedEventId = eventId; + + // Force table refresh + [self dataSource:self.roomDataSource didCellChange:nil]; } - (void)cancelEventSelection { - [self enableReplyMode:NO]; + [self setInputToolBarSendMode:RoomInputToolbarViewSendModeSend]; if (currentAlert) { @@ -2888,8 +2896,11 @@ - (void)cancelEventSelection currentAlert = nil; } + customizedRoomDataSource.showBubbleDateTimeOnSelection = YES; customizedRoomDataSource.selectedEventId = nil; + [self restoreTextMessageBeforeEditing]; + // Force table refresh [self dataSource:self.roomDataSource didCellChange:nil]; } @@ -2900,6 +2911,45 @@ - (void)showUnableToOpenLinkErrorAlert message:NSLocalizedStringFromTable(@"room_message_unable_open_link_error_message", @"Vector", nil)]; } +- (void)editEventContentWithId:(NSString*)eventId +{ + MXEvent *event = [self.roomDataSource eventWithEventId:eventId]; + + RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; + + if (roomInputToolbarView) + { + self.textMessageBeforeEditing = roomInputToolbarView.textMessage; + roomInputToolbarView.textMessage = [self.roomDataSource editableTextMessageForEvent:event]; + } + + [self selectEventWithId:eventId inputToolBarSendMode:RoomInputToolbarViewSendModeEdit showTimestamp:YES]; +} + +- (void)restoreTextMessageBeforeEditing +{ + RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; + + if (self.textMessageBeforeEditing) + { + roomInputToolbarView.textMessage = self.textMessageBeforeEditing; + } + + self.textMessageBeforeEditing = nil; +} + +- (RoomInputToolbarView*)inputToolbarViewAsRoomInputToolbarView +{ + RoomInputToolbarView *roomInputToolbarView; + + if (self.inputToolbarView && [self.inputToolbarView isKindOfClass:[RoomInputToolbarView class]]) + { + roomInputToolbarView = (RoomInputToolbarView*)self.inputToolbarView; + } + + return roomInputToolbarView; +} + #pragma mark - Segues - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender @@ -3307,6 +3357,17 @@ - (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView heightDidChan } } +- (void)roomInputToolbarViewDidTapFileUpload:(MXKRoomInputToolbarView *)toolbarView +{ + MXKDocumentPickerPresenter *documentPickerPresenter = [MXKDocumentPickerPresenter new]; + documentPickerPresenter.delegate = self; + + NSArray *allowedUTIs = @[MXKUTI.data]; + [documentPickerPresenter presentDocumentPickerWith:allowedUTIs from:self animated:YES completion:nil]; + + self.documentPickerPresenter = documentPickerPresenter; +} + #pragma mark - RoomParticipantsViewControllerDelegate - (void)roomParticipantsViewController:(RoomParticipantsViewController *)roomParticipantsViewController mention:(MXRoomMember*)member @@ -3637,7 +3698,7 @@ - (void)roomTitleView:(RoomTitleView*)titleView recognizeTapGesture:(UITapGestur } // Note in case of simple link to a room the signUrl param is nil - [self joinRoomWithRoomIdOrAlias:roomIdOrAlias andSignUrl:roomPreviewData.emailInvitation.signUrl completion:^(BOOL succeed) { + [self joinRoomWithRoomIdOrAlias:roomIdOrAlias viaServers:roomPreviewData.viaServers andSignUrl:roomPreviewData.emailInvitation.signUrl completion:^(BOOL succeed) { if (succeed) { @@ -3999,11 +4060,43 @@ - (void)refreshActivitiesViewDisplay } else if (customizedRoomDataSource.roomState.isObsolete) { - NSString *replacementRoomId = customizedRoomDataSource.roomState.tombStoneContent.replacementRoomId; - NSString *roomLinkFragment = [NSString stringWithFormat:@"/room/%@", [MXTools encodeURIComponent:replacementRoomId]]; - + MXWeakify(self); [roomActivitiesView displayRoomReplacementWithRoomLinkTappedHandler:^{ - [[AppDelegate theDelegate] handleUniversalLinkFragment:roomLinkFragment]; + MXStrongifyAndReturnIfNil(self); + + MXEvent *stoneTombEvent = [self->customizedRoomDataSource.roomState stateEventsWithType:kMXEventTypeStringRoomTombStone].lastObject; + + NSString *replacementRoomId = self->customizedRoomDataSource.roomState.tombStoneContent.replacementRoomId; + if ([self.roomDataSource.mxSession roomWithRoomId:replacementRoomId]) + { + // Open the room if it is already joined + [[AppDelegate theDelegate] showRoom:replacementRoomId andEventId:nil withMatrixSession:self.roomDataSource.mxSession]; + } + else + { + // Else auto join it via the server that sent the event + NSLog(@"[RoomVC] Auto join an upgraded room: %@ -> %@. Sender: %@", self->customizedRoomDataSource.roomState.roomId, + replacementRoomId, stoneTombEvent.sender); + + NSString *viaSenderServer = [MXTools serverNameInMatrixIdentifier:stoneTombEvent.sender]; + + if (viaSenderServer) + { + [self startActivityIndicator]; + [self.roomDataSource.mxSession joinRoom:replacementRoomId viaServers:@[viaSenderServer] success:^(MXRoom *room) { + [self stopActivityIndicator]; + + [[AppDelegate theDelegate] showRoom:replacementRoomId andEventId:nil withMatrixSession:self.roomDataSource.mxSession]; + + } failure:^(NSError *error) { + [self stopActivityIndicator]; + + NSLog(@"[RoomVC] Failed to join an upgraded room. Error: %@", + error); + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + } }]; } else if (customizedRoomDataSource.roomState.isOngoingConferenceCall) @@ -4488,6 +4581,18 @@ - (void)eventDidChangeSentState:(NSNotification *)notif } } +- (void)eventDidChangeIdentifier:(NSNotification *)notif +{ + MXEvent *event = notif.object; + NSString *previousId = notif.userInfo[kMXEventIdentifierKey]; + + if ([customizedRoomDataSource.selectedEventId isEqualToString:previousId]) + { + NSLog(@"[RoomVC] eventDidChangeIdentifier: Update selectedEventId"); + customizedRoomDataSource.selectedEventId = event.eventId; + } +} + - (void)resendAllUnsentMessages { @@ -4969,5 +5074,332 @@ - (void)removeMXSessionStateChangeNotificationsListener } } +#pragma mark - Contextual Menu + +- (NSArray*)contextualMenuItemsForEvent:(MXEvent*)event andCell:(id)cell +{ + NSString *eventId = event.eventId; + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; + MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment; + + MXWeakify(self); + + // Copy action + + RoomContextualMenuItem *copyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionCopy]; + copyMenuItem.isEnabled = !attachment || attachment.type != MXKAttachmentTypeSticker; + copyMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + + if (!attachment) + { + NSArray *components = roomBubbleTableViewCell.bubbleData.bubbleComponents; + MXKRoomBubbleComponent *selectedComponent; + for (selectedComponent in components) + { + if ([selectedComponent.event.eventId isEqualToString:event.eventId]) + { + break; + } + selectedComponent = nil; + } + NSString *textMessage = selectedComponent.textMessage; + + [UIPasteboard generalPasteboard].string = textMessage; + + [self hideContextualMenuAnimated:YES]; + } + else if (attachment.type != MXKAttachmentTypeSticker) + { + [self hideContextualMenuAnimated:YES completion:^{ + [self startActivityIndicator]; + + [attachment copy:^{ + + [self stopActivityIndicator]; + + } failure:^(NSError *error) { + + [self stopActivityIndicator]; + + //Alert user + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + + // Start animation in case of download during attachment preparing + [roomBubbleTableViewCell startProgressUI]; + }]; + } + }; + + // Reply action + + RoomContextualMenuItem *replyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionReply]; + replyMenuItem.isEnabled = [self.roomDataSource canReplyToEventWithId:eventId]; + replyMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + + [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; + [self selectEventWithId:eventId inputToolBarSendMode:RoomInputToolbarViewSendModeReply showTimestamp:NO]; + + // And display the keyboard + [self.inputToolbarView becomeFirstResponder]; + }; + + // Edit action + + RoomContextualMenuItem *editMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionEdit]; + editMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; + [self editEventContentWithId:eventId]; + + // And display the keyboard + [self.inputToolbarView becomeFirstResponder]; + }; + + editMenuItem.isEnabled = [self.roomDataSource canEditEventWithId:eventId]; + + // More action + + RoomContextualMenuItem *moreMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionMore]; + moreMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + [self hideContextualMenuAnimated:YES completion:nil]; + [self showAdditionalActionsMenuForEvent:event inCell:cell animated:YES]; + }; + + // Actions list + + NSArray *actionItems = @[ + copyMenuItem, + replyMenuItem, + editMenuItem, + moreMenuItem + ]; + + return actionItems; +} + +- (void)showContextualMenuForEvent:(MXEvent*)event fromSingleTapGesture:(BOOL)usedSingleTapGesture cell:(id)cell animated:(BOOL)animated +{ + if (self.roomContextualMenuPresenter.isPresenting) + { + return; + } + + NSString *selectedEventId = event.eventId; + + NSArray* contextualMenuItems = [self contextualMenuItemsForEvent:event andCell:cell]; + ReactionsMenuViewModel *reactionsMenuViewModel; + CGRect bubbleComponentFrameInOverlayView = CGRectNull; + + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && [self.roomDataSource canReactToEventWithId:event.eventId]) + { + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell; + 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; + }]; + + CGRect bubbleComponentFrame; + + if (bubbleComponents.count > 0) + { + NSInteger selectedComponentIndex = foundComponentIndex != NSNotFound ? foundComponentIndex : 0; + bubbleComponentFrame = [roomBubbleTableViewCell surroundingFrameInTableViewForComponentIndex:selectedComponentIndex]; + } + else + { + bubbleComponentFrame = roomBubbleTableViewCell.frame; + } + + bubbleComponentFrameInOverlayView = [self.bubblesTableView convertRect:bubbleComponentFrame toView:self.overlayContainerView]; + + NSString *roomId = self.roomDataSource.roomId; + MXAggregations *aggregations = self.mainSession.aggregations; + MXAggregatedReactions *aggregatedReactions = [aggregations aggregatedReactionsOnEvent:selectedEventId inRoom:roomId]; + + reactionsMenuViewModel = [[ReactionsMenuViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:selectedEventId]; + reactionsMenuViewModel.coordinatorDelegate = self; + } + + if (!self.roomContextualMenuViewController) + { + self.roomContextualMenuViewController = [RoomContextualMenuViewController instantiate]; + self.roomContextualMenuViewController.delegate = self; + } + + [self.roomContextualMenuViewController updateWithContextualMenuItems:contextualMenuItems reactionsMenuViewModel:reactionsMenuViewModel]; + + [self enableOverlayContainerUserInteractions:YES]; + + [self.roomContextualMenuPresenter presentWithRoomContextualMenuViewController:self.roomContextualMenuViewController + from:self + on:self.overlayContainerView + contentToReactFrame:bubbleComponentFrameInOverlayView + fromSingleTapGesture:usedSingleTapGesture + animated:animated + completion:^{ + }]; + + [self selectEventWithId:selectedEventId]; +} + +- (void)hideContextualMenuAnimated:(BOOL)animated +{ + [self hideContextualMenuAnimated:animated completion:nil]; +} + +- (void)hideContextualMenuAnimated:(BOOL)animated completion:(void(^)(void))completion +{ + [self hideContextualMenuAnimated:animated cancelEventSelection:YES completion:completion]; +} + +- (void)hideContextualMenuAnimated:(BOOL)animated cancelEventSelection:(BOOL)cancelEventSelection completion:(void(^)(void))completion +{ + if (!self.roomContextualMenuPresenter.isPresenting) + { + return; + } + + if (cancelEventSelection) + { + [self cancelEventSelection]; + } + + [self.roomContextualMenuPresenter hideContextualMenuWithAnimated:animated completion:^{ + [self enableOverlayContainerUserInteractions:NO]; + + if (completion) + { + completion(); + } + }]; +} + +- (void)enableOverlayContainerUserInteractions:(BOOL)enableOverlayContainerUserInteractions +{ + self.inputToolbarView.editable = !enableOverlayContainerUserInteractions; + self.bubblesTableView.scrollsToTop = !enableOverlayContainerUserInteractions; + self.overlayContainerView.userInteractionEnabled = enableOverlayContainerUserInteractions; +} + +#pragma mark - RoomContextualMenuViewControllerDelegate + +- (void)roomContextualMenuViewControllerDidTapBackgroundOverlay:(RoomContextualMenuViewController *)viewController +{ + [self hideContextualMenuAnimated:YES]; +} + +#pragma mark - ReactionsMenuViewModelCoordinatorDelegate + +- (void)reactionsMenuViewModel:(ReactionsMenuViewModel *)viewModel didAddReaction:(NSString *)reaction forEventId:(NSString *)eventId +{ + MXWeakify(self); + + [self hideContextualMenuAnimated:YES completion:^{ + + [self.roomDataSource addReaction:reaction forEventId:eventId success:^{ + + } failure:^(NSError *error) { + MXStrongifyAndReturnIfNil(self); + + [self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil]; + }]; + }]; +} + +- (void)reactionsMenuViewModel:(ReactionsMenuViewModel *)viewModel didRemoveReaction:(NSString *)reaction forEventId:(NSString *)eventId +{ + MXWeakify(self); + + [self hideContextualMenuAnimated:YES completion:^{ + + [self.roomDataSource removeReaction:reaction forEventId:eventId success:^{ + + } failure:^(NSError *error) { + MXStrongifyAndReturnIfNil(self); + + [self.errorPresenter presentErrorFromViewController:self forError:error animated:YES handler:nil]; + }]; + + }]; +} + +#pragma mark - + +- (void)showEditHistoryForEventId:(NSString*)eventId animated:(BOOL)animated +{ + MXEvent *event = [self.roomDataSource eventWithEventId:eventId]; + EditHistoryCoordinatorBridgePresenter *presenter = [[EditHistoryCoordinatorBridgePresenter alloc] initWithSession:self.roomDataSource.mxSession event:event]; + + presenter.delegate = self; + [presenter presentFrom:self animated:animated]; + + self.editHistoryPresenter = presenter; +} + +#pragma mark - EditHistoryCoordinatorBridgePresenterDelegate + +- (void)editHistoryCoordinatorBridgePresenterDelegateDidComplete:(EditHistoryCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + self.editHistoryPresenter = nil; +} + +#pragma mark - DocumentPickerPresenterDelegate + +- (void)documentPickerPresenterWasCancelled:(MXKDocumentPickerPresenter *)presenter +{ + self.documentPickerPresenter = nil; +} + +- (void)documentPickerPresenter:(MXKDocumentPickerPresenter *)presenter didPickDocumentsAt:(NSURL *)url +{ + self.documentPickerPresenter = nil; + + MXKUTI *fileUTI = [[MXKUTI alloc] initWithLocalFileURL:url]; + NSString *mimeType = fileUTI.mimeType; + + if (fileUTI.isImage) + { + NSData *imageData = [[NSData alloc] initWithContentsOfURL:url]; + + [self.roomDataSource sendImage:imageData mimeType:mimeType success:nil failure:^(NSError *error) { + // Nothing to do. The image is marked as unsent in the room history by the datasource + NSLog(@"[MXKRoomViewController] sendImage failed."); + }]; + } + else if (fileUTI.isVideo) + { + [(RoomDataSource*)self.roomDataSource sendVideo:url success:nil failure:^(NSError *error) { + // Nothing to do. The video is marked as unsent in the room history by the datasource + NSLog(@"[MXKRoomViewController] sendVideo failed."); + }]; + } + else if (fileUTI.isFile) + { + [self.roomDataSource sendFile:url mimeType:mimeType success:nil failure:^(NSError *error) { + // Nothing to do. The file is marked as unsent in the room history by the datasource + NSLog(@"[MXKRoomViewController] sendFile failed."); + }]; + } + else + { + NSLog(@"[MXKRoomViewController] File upload using MIME type %@ is not supported.", mimeType); + + [[AppDelegate theDelegate] showAlertWithTitle:NSLocalizedStringFromTable(@"file_upload_error_title", @"Vector", nil) + message:NSLocalizedStringFromTable(@"file_upload_error_unsupported_file_type_message", @"Vector", nil)]; + } +} + @end diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index 0a8a14425f..6ea9ec67fd 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -1,11 +1,11 @@ - + - + @@ -21,6 +21,7 @@ + @@ -130,7 +131,7 @@ - + @@ -146,10 +147,15 @@ + + + + + @@ -160,12 +166,15 @@ + + + @@ -174,7 +183,7 @@ - - + + diff --git a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift new file mode 100644 index 0000000000..dee83d9855 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift @@ -0,0 +1,48 @@ +/* + 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 + +/// MXKRoomBubbleTableViewCell layout constants +@objcMembers +final class RoomBubbleCellLayout: NSObject { + + // Reactions + + static let reactionsViewTopMargin: CGFloat = 1.0 + static let reactionsViewLeftMargin: CGFloat = 55.0 + static let reactionsViewRightMargin: CGFloat = 15.0 + + // Read receipts + + static let readReceiptsViewTopMargin: CGFloat = 5.0 + static let readReceiptsViewRightMargin: CGFloat = 6.0 + static let readReceiptsViewHeight: CGFloat = 12.0 + static let readReceiptsViewWidth: CGFloat = 150.0 + + // Read marker + + static let readMarkerViewHeight: CGFloat = 2.0 + + // Timestamp + + static let timestampLabelHeight: CGFloat = 18.0 + static let timestampLabelWidth: CGFloat = 39.0 + + // Others + + static let encryptedContentLeftMargin: CGFloat = 15.0 +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomEncryptedDataBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomEncryptedDataBubbleCell.m index 77df69f6b6..ab4c455e5d 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomEncryptedDataBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomEncryptedDataBubbleCell.m @@ -29,7 +29,8 @@ + (UIImage*)encryptionIconForEvent:(MXEvent*)event andSession:(MXSession*)sessio { encryptionIcon = @"e2e_unencrypted"; - if (event.isLocalEvent) + if (event.isLocalEvent + || event.contentHasBeenEdited) // Local echo for an edit is clear but uses a true event id, the one of the edited event { // Patch: Display the verified icon by default on pending outgoing messages in the encrypted rooms when the encryption is enabled MXRoom *room = [session roomWithRoomId:event.roomId]; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentBubbleCell.m index 00018fb4de..2f7e4d80aa 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentBubbleCell.m @@ -39,4 +39,16 @@ - (void)render:(MXKCellData *)cellData [self updateUserNameColor]; } ++ (CGFloat)heightForCellData:(MXKCellData*)cellData withMaximumWidth:(CGFloat)maxWidth +{ + CGFloat rowHeight = [self attachmentBubbleCellHeightForCellData:cellData withMaximumWidth:maxWidth]; + + if (rowHeight <= 0) + { + rowHeight = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + } + + return rowHeight; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m index 519bff8725..16a614a460 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m @@ -45,4 +45,16 @@ - (void)render:(MXKCellData *)cellData } } ++ (CGFloat)heightForCellData:(MXKCellData*)cellData withMaximumWidth:(CGFloat)maxWidth +{ + CGFloat rowHeight = [self attachmentBubbleCellHeightForCellData:cellData withMaximumWidth:maxWidth]; + + if (rowHeight <= 0) + { + rowHeight = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + } + + return rowHeight; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m index 7123851524..5d63baf003 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m @@ -16,6 +16,7 @@ */ #import "RoomIncomingAttachmentWithoutSenderInfoBubbleCell.h" +#import "MXKRoomBubbleTableViewCell+Riot.h" #import "ThemeService.h" #import "Riot-Swift.h" @@ -29,4 +30,16 @@ - (void)customizeTableViewCellRendering self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } ++ (CGFloat)heightForCellData:(MXKCellData*)cellData withMaximumWidth:(CGFloat)maxWidth +{ + CGFloat rowHeight = [self attachmentBubbleCellHeightForCellData:cellData withMaximumWidth:maxWidth]; + + if (rowHeight <= 0) + { + rowHeight = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + } + + return rowHeight; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingAttachmentBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingAttachmentBubbleCell.m index a50ffe6c99..e6417201bc 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingAttachmentBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingAttachmentBubbleCell.m @@ -62,4 +62,16 @@ + (void)render:(MXKCellData *)cellData inBubbleCell:(MXKRoomOutgoingAttachmentBu } } ++ (CGFloat)heightForCellData:(MXKCellData*)cellData withMaximumWidth:(CGFloat)maxWidth +{ + CGFloat rowHeight = [self attachmentBubbleCellHeightForCellData:cellData withMaximumWidth:maxWidth]; + + if (rowHeight <= 0) + { + rowHeight = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + } + + return rowHeight; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m index b60dac6771..30a50ae2d2 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m @@ -19,6 +19,8 @@ #import "ThemeService.h" #import "Riot-Swift.h" +#import "RoomBubbleCellData.h" +#import "MXKRoomBubbleTableViewCell+Riot.h" @implementation RoomOutgoingAttachmentWithoutSenderInfoBubbleCell @@ -36,4 +38,16 @@ - (void)render:(MXKCellData *)cellData [RoomOutgoingAttachmentBubbleCell render:cellData inBubbleCell:self]; } ++ (CGFloat)heightForCellData:(MXKCellData*)cellData withMaximumWidth:(CGFloat)maxWidth +{ + CGFloat rowHeight = [self attachmentBubbleCellHeightForCellData:cellData withMaximumWidth:maxWidth]; + + if (rowHeight <= 0) + { + rowHeight = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + } + + return rowHeight; +} + @end diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index 20c66fcafe..fdff609e19 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -18,6 +18,17 @@ #import "MediaPickerViewController.h" +/** + Destination of the message in the composer + */ +typedef enum : NSUInteger +{ + RoomInputToolbarViewSendModeSend, + RoomInputToolbarViewSendModeReply, + RoomInputToolbarViewSendModeEdit +} RoomInputToolbarViewSendMode; + + @protocol RoomInputToolbarViewDelegate /** @@ -27,6 +38,13 @@ */ - (void)roomInputToolbarViewPresentStickerPicker:(MXKRoomInputToolbarView*)toolbarView; +/** + Tells the delegate that the user wants to send external files. + + @param toolbarView the room input toolbar view + */ +- (void)roomInputToolbarViewDidTapFileUpload:(MXKRoomInputToolbarView*)toolbarView; + @end /** @@ -38,7 +56,7 @@ /** The delegate notified when inputs are ready. */ -@property (nonatomic) id delegate; +@property (nonatomic, weak) id delegate; @property (weak, nonatomic) IBOutlet UIView *mainToolbarView; @@ -70,9 +88,9 @@ @property (nonatomic) BOOL isEncryptionEnabled; /** - Tell whether the input text will be a reply to a message. + Destination of the message in the composer. */ -@property (nonatomic, getter=isReplyToEnabled) BOOL replyToEnabled; +@property (nonatomic) RoomInputToolbarViewSendMode sendMode; /** Tell whether a call is active. diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 0b483bd5c4..f09f0ba8fe 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -69,6 +69,7 @@ - (void)awakeFromNib [super awakeFromNib]; _supportCallOption = YES; + _sendMode = RoomInputToolbarViewSendModeSend; self.rightInputToolbarButton.hidden = YES; @@ -154,11 +155,33 @@ - (void)setIsEncryptionEnabled:(BOOL)isEncryptionEnabled self.placeholder = placeholder; } -- (void)setReplyToEnabled:(BOOL)isReplyToEnabled +- (void)setSendMode:(RoomInputToolbarViewSendMode)sendMode { - _replyToEnabled = isReplyToEnabled; - + _sendMode = sendMode; + [self updatePlaceholder]; + [self updateToolbarButtonLabel]; +} + +- (void)updateToolbarButtonLabel +{ + NSString *title; + + switch (_sendMode) + { + case RoomInputToolbarViewSendModeReply: + title = NSLocalizedStringFromTable(@"room_action_reply", @"Vector", nil); + break; + case RoomInputToolbarViewSendModeEdit: + title = NSLocalizedStringFromTable(@"save", @"Vector", nil); + break; + default: + title = [NSBundle mxk_localizedStringForKey:@"send"]; + break; + } + + [self.rightInputToolbarButton setTitle:title forState:UIControlStateNormal]; + [self.rightInputToolbarButton setTitle:title forState:UIControlStateHighlighted]; } - (void)updatePlaceholder @@ -172,17 +195,44 @@ - (void)updatePlaceholder if (!shouldDisplayLargePlaceholder) { - placeholder = _replyToEnabled ? NSLocalizedStringFromTable(@"room_message_reply_to_short_placeholder", @"Vector", nil) : NSLocalizedStringFromTable(@"room_message_short_placeholder", @"Vector", nil); + switch (_sendMode) + { + case RoomInputToolbarViewSendModeReply: + placeholder = NSLocalizedStringFromTable(@"room_message_reply_to_short_placeholder", @"Vector", nil); + break; + + default: + placeholder = NSLocalizedStringFromTable(@"room_message_short_placeholder", @"Vector", nil); + break; + } } else { if (_isEncryptionEnabled) { - placeholder = _replyToEnabled ? NSLocalizedStringFromTable(@"encrypted_room_message_reply_to_placeholder", @"Vector", nil) : NSLocalizedStringFromTable(@"encrypted_room_message_placeholder", @"Vector", nil); + switch (_sendMode) + { + case RoomInputToolbarViewSendModeReply: + placeholder = NSLocalizedStringFromTable(@"encrypted_room_message_reply_to_placeholder", @"Vector", nil); + break; + + default: + placeholder = NSLocalizedStringFromTable(@"encrypted_room_message_placeholder", @"Vector", nil); + break; + } } else { - placeholder = _replyToEnabled ? NSLocalizedStringFromTable(@"room_message_reply_to_placeholder", @"Vector", nil) : NSLocalizedStringFromTable(@"room_message_placeholder", @"Vector", nil); + switch (_sendMode) + { + case RoomInputToolbarViewSendModeReply: + placeholder = NSLocalizedStringFromTable(@"room_message_reply_to_placeholder", @"Vector", nil); + break; + + default: + placeholder = NSLocalizedStringFromTable(@"room_message_placeholder", @"Vector", nil); + break; + } } } @@ -299,6 +349,19 @@ - (IBAction)onTouchUpInside:(UIButton*)button } }]]; + + [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"room_action_send_file", @"Vector", nil) + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + if (weakSelf) + { + typeof(self) self = weakSelf; + self->actionSheet = nil; + + [self.delegate roomInputToolbarViewDidTapFileUpload:self]; + } + }]]; [actionSheet addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] style:UIAlertActionStyleCancel diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index cbda06f84f..b8e545a09c 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -119,7 +119,7 @@ enum { LABS_USE_ROOM_MEMBERS_LAZY_LOADING_INDEX = 0, - LABS_USE_JITSI_WIDGET_INDEX = 0, + LABS_USE_JITSI_WIDGET_INDEX, LABS_CRYPTO_INDEX, LABS_COUNT }; @@ -2135,7 +2135,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleJitsiForConference:) forControlEvents:UIControlEventTouchUpInside]; cell = labelAndSwitchCell; - } + } else if (row == LABS_CRYPTO_INDEX) { MXSession* session = [AppDelegate theDelegate].mxSessions[0]; @@ -2874,7 +2874,11 @@ - (void)togglePushNotifications:(id)sender if (accountManager.pushDeviceToken) { - [account setEnablePushKitNotifications:!account.isPushKitNotificationActive]; + [account enablePushKitNotifications:!account.isPushKitNotificationActive success:^{ + [self stopActivityIndicator]; + } failure:^(NSError *error) { + [self stopActivityIndicator]; + }]; } else { @@ -2887,7 +2891,11 @@ - (void)togglePushNotifications:(id)sender } else { - [account setEnablePushKitNotifications:YES]; + [account enablePushKitNotifications:YES success:^{ + [self stopActivityIndicator]; + } failure:^(NSError *error) { + [self stopActivityIndicator]; + }]; } }]; } diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist index f2e6a76216..b69ed44c82 100644 --- a/Riot/SupportingFiles/Info.plist +++ b/Riot/SupportingFiles/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.8.6 + 0.9.0 CFBundleSignature ???? CFBundleVersion - 0.8.6 + 0.9.0 ITSAppUsesNonExemptEncryption ITSEncryptionExportComplianceCode diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 7467821ebb..68348718c3 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -12,3 +12,4 @@ #import "RecentsDataSource.h" #import "AvatarGenerator.h" #import "EncryptionInfoView.h" +#import "EventFormatter.h" diff --git a/Riot/Utils/EventFormatter.h b/Riot/Utils/EventFormatter.h index d55c032570..4952f14412 100644 --- a/Riot/Utils/EventFormatter.h +++ b/Riot/Utils/EventFormatter.h @@ -19,18 +19,41 @@ /** Link string used in attributed strings to mark a keys re-request action. */ -FOUNDATION_EXPORT NSString *const kEventFormatterOnReRequestKeysLinkAction; +FOUNDATION_EXPORT NSString *const EventFormatterOnReRequestKeysLinkAction; /** Parameters separator in the link string. */ -FOUNDATION_EXPORT NSString *const kEventFormatterOnReRequestKeysLinkActionSeparator; +FOUNDATION_EXPORT NSString *const EventFormatterLinkActionSeparator; + +/** + Link string used in attributed strings to mark an edited event action. + */ +FOUNDATION_EXPORT NSString *const EventFormatterEditedEventLinkAction; /** `EventFormatter` class inherits from `MXKEventFormatter` to define Vector formatting */ @interface EventFormatter : MXKEventFormatter +/** + Add a "(edited)" mention to edited message. + Default is YES. + */ +@property (nonatomic) BOOL showEditionMention; + +/** + Text color used to display message edited mention. + Default is `textSecondaryColor`. + */ +@property (nonatomic) UIColor *editionMentionTextColor; + +/** + Text font used to display message edited mention. + Default is system font 12. + */ +@property (nonatomic) UIFont *editionMentionTextFont; + /** String attributes for event timestamp displayed in chat history. */ diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 7926f11cfc..3ba09e3183 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -27,8 +27,11 @@ #pragma mark - Constants definitions -NSString *const kEventFormatterOnReRequestKeysLinkAction = @"kEventFormatterOnReRequestKeysLinkAction"; -NSString *const kEventFormatterOnReRequestKeysLinkActionSeparator = @"/"; +NSString *const EventFormatterOnReRequestKeysLinkAction = @"EventFormatterOnReRequestKeysLinkAction"; +NSString *const EventFormatterLinkActionSeparator = @"/"; +NSString *const EventFormatterEditedEventLinkAction = @"EventFormatterEditedEventLinkAction"; + +static NSString *const kEventFormatterTimeFormat = @"HH:mm"; @interface EventFormatter () { @@ -41,6 +44,14 @@ @interface EventFormatter () @implementation EventFormatter +- (void)initDateTimeFormatters +{ + [super initDateTimeFormatters]; + + timeFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + [timeFormatter setDateFormat:kEventFormatterTimeFormat]; +} + - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState error:(MXKEventFormatterError *)error { // Build strings for widget events @@ -149,8 +160,8 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState NSMutableAttributedString *attributedStringWithRerequestMessage = [attributedString mutableCopy]; [attributedStringWithRerequestMessage appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]]; - NSString *linkActionString = [NSString stringWithFormat:@"%@%@%@", kEventFormatterOnReRequestKeysLinkAction, - kEventFormatterOnReRequestKeysLinkActionSeparator, + NSString *linkActionString = [NSString stringWithFormat:@"%@%@%@", EventFormatterOnReRequestKeysLinkAction, + EventFormatterLinkActionSeparator, event.eventId]; [attributedStringWithRerequestMessage appendAttributedString: @@ -171,6 +182,26 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState attributedString = attributedStringWithRerequestMessage; } } + else if (self.showEditionMention && event.contentHasBeenEdited) + { + NSMutableAttributedString *attributedStringWithEditMention = [attributedString mutableCopy]; + + NSString *linkActionString = [NSString stringWithFormat:@"%@%@%@", EventFormatterEditedEventLinkAction, + EventFormatterLinkActionSeparator, + event.eventId]; + + [attributedStringWithEditMention appendAttributedString: + [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", NSLocalizedStringFromTable(@"event_formatter_message_edited_mention", @"Vector", nil)] + attributes:@{ + NSLinkAttributeName: linkActionString, + // NOTE: Color is curretly overidden by UIText.tintColor as we use `NSLinkAttributeName`. + // If we use UITextView.linkTextAttributes to set link color we will also have the issue that color will be the same for all kind of links. + NSForegroundColorAttributeName: self.editionMentionTextColor, + NSFontAttributeName: self.editionMentionTextFont + }]]; + + attributedString = attributedStringWithEditMention; + } return attributedString; } @@ -224,6 +255,8 @@ - (instancetype)initWithMatrixSession:(MXSession *)matrixSession self.encryptingTextColor = ThemeService.shared.theme.tintColor; self.sendingTextColor = ThemeService.shared.theme.textSecondaryColor; self.errorTextColor = ThemeService.shared.theme.warningColor; + self.showEditionMention = YES; + self.editionMentionTextColor = ThemeService.shared.theme.textSecondaryColor; self.defaultTextFont = [UIFont systemFontOfSize:15]; self.prefixTextFont = [UIFont boldSystemFontOfSize:15]; @@ -232,6 +265,7 @@ - (instancetype)initWithMatrixSession:(MXSession *)matrixSession self.callNoticesTextFont = [UIFont italicSystemFontOfSize:15]; self.encryptedMessagesTextFont = [UIFont italicSystemFontOfSize:15]; self.emojiOnlyTextFont = [UIFont systemFontOfSize:48]; + self.editionMentionTextFont = [UIFont systemFontOfSize:12]; } return self; } diff --git a/RiotShareExtension/SupportingFiles/Info.plist b/RiotShareExtension/SupportingFiles/Info.plist index e71361f1c5..981a683662 100644 --- a/RiotShareExtension/SupportingFiles/Info.plist +++ b/RiotShareExtension/SupportingFiles/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.8.6 + 0.9.0 CFBundleVersion - 0.8.6 + 0.9.0 NSExtension NSExtensionAttributes @@ -28,6 +28,8 @@ NSExtensionActivationDictionaryVersion 2 + NSExtensionActivationSupportsFileWithMaxCount + 5 NSExtensionActivationSupportsImageWithMaxCount 5 NSExtensionActivationSupportsMovieWithMaxCount diff --git a/SiriIntents/Info.plist b/SiriIntents/Info.plist index dfd30fd667..b31f06aa7b 100644 --- a/SiriIntents/Info.plist +++ b/SiriIntents/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.8.6 + 0.9.0 CFBundleVersion - 0.8.6 + 0.9.0 NSExtension NSExtensionAttributes diff --git a/Tools/Logs/filterCryptoLogs.sh b/Tools/Logs/filterCryptoLogs.sh new file mode 100755 index 0000000000..72363f7835 --- /dev/null +++ b/Tools/Logs/filterCryptoLogs.sh @@ -0,0 +1,59 @@ +#!/bin/sh + +# Filter Riot logs to extract only logs related to end-to-end encryption. +# The output is colorised according to the cryto sub module. +# +# Usage: +# ./filterCryptoLogs.sh console.log + +FILES=$1 + +if [ ! -n "$FILES" ]; then + FILES="*" +fi + +grep -iE 'crypto|MXDevice|olm|error|MXKey|KeyRequest' $FILES \ + | grep -viE 'MXJSONModels|MXOlmSessionResult|MXRealmCryptoStore|NSCocoaErrorDomain|olm_keys_not_sent_error' \ + | awk '{ + # Errors in red (I failed to make a gsub case insensitive) + gsub(".*error.*", "\033[0;31m&\033[0m"); + gsub(".*Error.*", "\033[0;31m&\033[0m"); + gsub(".*ERROR.*", "\033[0;31m&\033[0m"); + + # Isolate each encryption of a message + gsub(".*\\[MXRoom] sendEventOfType\\(MXCrypto\\)\\: Encrypting event.*", + "\n\n\n\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n&"); + gsub(".*\\[MXRoom] sendEventOfType\\(MXCrypto\\)\\: Send event.*", + "&\n----------------------------------------------------------------------------------------------------------------\n\n\n\n"); + gsub(".*\\[MXRoom] sendEventOfType\\(MXCrypto\\)\\: Cannot encrypt.*", + "&\n----------------------------------------------------------------------------------------------------------------\n\n\n\n"); + + gsub("\\[MXCrypto\\]", "\033[0;32m&\033[0m"); + gsub("\\[MXOlmDevice\\]", "\033[0;33m&\033[0m"); + + gsub("\\[MXMegolmEncryption\\]", "\033[0;36m&\033[0m"); + + gsub("\\[MXOlmInboundGroupSession\\]", "\033[1;34m&\033[0m"); + gsub("\\[MXMegolmDecryption\\]", "\033[0;34m&\033[0m"); + + gsub("\\[MXOlmDecryption\\]", "\033[0;34m&\033[0m"); + + gsub("\\[MXDeviceList\\]", "\033[0;36m&\033[0m"); + gsub("\\[MXDeviceListOperationsPool\\]", "\033[1;36m&\033[0m"); + + gsub("\\[MXKey\\]", "\033[1;35m&\033[0m"); + gsub("\\[MXKeyBackup\\]", "\033[1;35m&\033[0m"); + gsub("\\[MXKeyBackupPassword\\]", "\033[1;35m&\033[0m"); + gsub("\\[MXMegolmExportEncryption\\]", "\033[1;35m&\033[0m"); + gsub("\\[MXKeyBackupPassword\\]", "\033[1;35m&\033[0m"); + + gsub("\\[MXOutgoingRoomKeyRequestManager\\]", "\033[1;35m&\033[0m"); + gsub("\\[MXIncomingRoomKeyRequestManager\\]", "\033[1;34m&\033[0m"); + + gsub("\\[MXDeviceVerificationTransaction\\]", "\033[0;36m&\033[0m"); + gsub("\\[MXKeyVerification\\]", "\033[0;36m&\033[0m"); + + gsub("\\[MXRealmCryptoStore\\]", "\033[0;37m&\033[0m"); + + print + }' diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift index 4d1e3b3c67..cc23f78f3b 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift @@ -77,10 +77,6 @@ final class TemplateScreenViewController: UIViewController { self.keyboardAvoider?.startAvoiding() } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) @@ -88,10 +84,6 @@ final class TemplateScreenViewController: UIViewController { self.keyboardAvoider?.stopAvoiding() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - override var preferredStatusBarStyle: UIStatusBarStyle { return self.theme.statusBarStyle }