diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 24e8ab2e6..68a169237 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -11,6 +11,7 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: build: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 00d4fa2f9..3fcc19ed3 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -12,6 +12,7 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: tests: diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index dfbd7ac6a..7ab8b331d 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -11,6 +11,7 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: build: diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index d07f6efb5..ccdafb43f 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -15,6 +15,7 @@ // import Foundation +import Keys /// BuildSettings provides settings computed at build time. /// In future, it may be automatically generated from xcconfig files @@ -22,13 +23,6 @@ import Foundation final class BuildSettings: NSObject { // MARK: - Bundle Settings - static var bundleDisplayName: String { - guard let bundleDisplayName = Bundle.app.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String else { - fatalError("CFBundleDisplayName should be defined") - } - return bundleDisplayName - } - static var applicationGroupIdentifier: String { guard let applicationGroupIdentifier = Bundle.app.object(forInfoDictionaryKey: "applicationGroupIdentifier") as? String else { fatalError("applicationGroupIdentifier should be defined") @@ -364,4 +358,16 @@ final class BuildSettings: NSObject { return true } + + // MARK: - Location Sharing + + static let tileServerMapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! + + static var locationSharingEnabled: Bool { + guard #available(iOS 14, *) else { + return false + } + + return false + } } diff --git a/Gemfile b/Gemfile index 53efbaf92..ea061a17e 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source "https://rubygems.org" gem "xcode-install" gem "fastlane" gem "cocoapods", '~>1.11.2' +gem "cocoapods-keys" 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 a8674758c..78fe028a3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,12 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.4) + CFPropertyList (3.0.5) rexml - activesupport (6.1.4.1) + RubyInline (3.12.5) + ZenTest (~> 4.3) + ZenTest (4.12.0) + activesupport (6.1.4.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -17,17 +20,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.510.0) - aws-sdk-core (3.121.1) + aws-partitions (1.541.0) + aws-sdk-core (3.124.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.48.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-kms (1.52.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.103.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-s3 (1.109.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -64,6 +67,9 @@ GEM typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) cocoapods-downloader (1.5.1) + cocoapods-keys (2.2.1) + dotenv + osx_keychain cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -84,9 +90,9 @@ GEM dotenv (2.7.6) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.14.0) + ethon (0.15.0) ffi (>= 1.15.0) - excon (0.86.0) + excon (0.89.0) faraday (1.8.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -109,10 +115,10 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday_middleware (1.1.0) + faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.5) - fastlane (2.195.0) + fastimage (2.2.6) + fastlane (2.199.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -161,7 +167,7 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.11.0) + google-apis-androidpublisher_v3 (0.14.0) google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.1) addressable (~> 2.5, >= 2.5.1) @@ -172,11 +178,11 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.7.0) + google-apis-iamcredentials_v1 (0.9.0) google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.5.0) + google-apis-playcustomapp_v1 (0.6.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.8.0) + google-apis-storage_v1 (0.10.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) @@ -184,15 +190,15 @@ GEM google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.34.1) - addressable (~> 2.5) + google-cloud-storage (1.35.0) + addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) google-apis-storage_v1 (~> 0.1) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.0.0) + googleauth (1.1.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -204,18 +210,18 @@ GEM http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.8.10) + i18n (1.8.11) concurrent-ruby (~> 1.0) jmespath (1.4.0) - json (2.5.1) + json (2.6.1) jwt (2.3.0) memoist (0.16.2) - mime-types (3.3.1) + mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2021.0901) + mime-types-data (3.2021.1115) mini_magick (4.11.0) - mini_mime (1.1.1) - minitest (5.14.4) + mini_mime (1.1.2) + minitest (5.15.0) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.0.0) @@ -224,7 +230,9 @@ GEM naturally (2.2.1) netrc (0.11.0) optparse (0.1.1) - os (1.1.1) + os (1.1.4) + osx_keychain (1.0.2) + RubyInline (~> 3) plist (3.6.0) public_suffix (4.0.6) rake (13.0.6) @@ -255,7 +263,7 @@ GEM terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - trailblazer-option (0.1.1) + trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) @@ -285,13 +293,14 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.4.2) + zeitwerk (2.5.1) PLATFORMS ruby DEPENDENCIES cocoapods (~> 1.11.2) + cocoapods-keys fastlane fastlane-plugin-diawi fastlane-plugin-versioning diff --git a/Podfile b/Podfile index 714154461..e465a8f52 100644 --- a/Podfile +++ b/Podfile @@ -95,7 +95,7 @@ abstract_target 'RiotPods' do pod 'SwiftJWT', '~> 3.6.200' pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' - pod 'ffmpeg-kit-ios-audio', '~> 4.5' + pod 'ffmpeg-kit-ios-audio', '4.5.1' pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] @@ -129,6 +129,10 @@ abstract_target 'RiotPods' do end +plugin 'cocoapods-keys', { + :project => "Riot", + :keys => ["MapTilerAPIKey"] +} post_install do |installer| installer.pods_project.targets.each do |target| diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json new file mode 100644 index 000000000..5bb98bf57 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "action_location.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "action_location@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "action_location@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png new file mode 100644 index 000000000..be968b77d Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@2x.png new file mode 100644 index 000000000..b77c9e11b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png new file mode 100644 index 000000000..8ce3cacae Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json new file mode 100644 index 000000000..707b2f06b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_marker_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_marker_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_marker_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png new file mode 100644 index 000000000..be968b77d Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@2x.png new file mode 100644 index 000000000..b77c9e11b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png new file mode 100644 index 000000000..8ce3cacae Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json new file mode 100644 index 000000000..c3b8dbc7a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_share_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_share_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_share_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png new file mode 100644 index 000000000..da7d7530b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@2x.png new file mode 100644 index 000000000..41969eb70 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@3x.png new file mode 100644 index 000000000..43bf9dcb3 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json new file mode 100644 index 000000000..175219374 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_user_marker.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_user_marker@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_user_marker@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png new file mode 100644 index 000000000..eedadad5e Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@2x.png new file mode 100644 index 000000000..fce4dcb04 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png new file mode 100644 index 000000000..ff8ce3aad Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png differ diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings index 6d3378a2a..a9513a6ed 100644 --- a/Riot/Assets/en.lproj/InfoPlist.strings +++ b/Riot/Assets/en.lproj/InfoPlist.strings @@ -21,3 +21,4 @@ "NSContactsUsageDescription" = "Element will show your contacts so you can invite them to chat."; "NSCalendarsUsageDescription" = "See your scheduled meetings in the app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app."; +"NSLocationWhenInUseUsageDescription" = "When you share your location to people, Element needs access to show them a map."; diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 914bb11e2..f3547d667 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -71,6 +71,9 @@ /* New file message from a specific person, not referencing a room. */ "FILE_FROM_USER" = "%@ sent a file %@"; +/* New file message from a specific person, not referencing a room. */ +"LOCATION_FROM_USER" = "%@ shared their location"; + /* A single unread message in a room */ "SINGLE_UNREAD_IN_ROOM" = "You received a message in %@"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index cc0d786d0..c51d27068 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -69,6 +69,7 @@ "private" = "Private"; "public" = "Public"; "stop" = "Stop"; +"ok" = "OK"; // Call Bar "callbar_only_single_active" = "Tap to return to the call (%@)"; @@ -952,7 +953,7 @@ Tap the + to start adding people."; // Analytics "analytics_prompt_title" = "Help improve %@"; -"analytics_prompt_message_new_user" = "Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +"analytics_prompt_message_new_user" = "Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; "analytics_prompt_message_upgrade" = "You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; /* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ "analytics_prompt_terms_new_user" = "You can read all our terms %@."; @@ -1756,7 +1757,7 @@ Tap the + to start adding people."; "spaces_coming_soon_title" = "Coming soon"; "spaces_add_rooms_coming_soon_title" = "Adding rooms coming soon"; "spaces_invites_coming_soon_title" = "Invites coming soon"; -"spaces_coming_soon_detail" = "This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer."; +"spaces_coming_soon_detail" = "This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with %@ on your computer."; "space_participants_action_remove" = "Remove from this space"; "space_participants_action_ban" = "Ban from this space"; "space_home_show_all_rooms" = "Show all rooms"; @@ -1885,8 +1886,6 @@ Tap the + to start adding people."; "poll_edit_form_post_failure_subtitle" = "Please try again"; -"poll_edit_form_post_failure_action" = "OK"; - "poll_timeline_one_vote" = "1 vote"; "poll_timeline_votes_count" = "%lu votes"; @@ -1909,10 +1908,32 @@ Tap the + to start adding people."; "poll_timeline_vote_not_registered_subtitle" = "Sorry, your vote was not registered, please try again"; -"poll_timeline_vote_not_registered_action" = "OK"; - "poll_timeline_not_closed_title" = "Failed to end poll"; "poll_timeline_not_closed_subtitle" = "Please try again"; -"poll_timeline_not_closed_action" = "OK"; +// MARK: - Location sharing + +"location_sharing_title" = "Location"; + +"location_sharing_close_action" = "Close"; + +"location_sharing_share_action" = "Share"; + +"location_sharing_loading_map_error_title" = "%@ could not load the map. Please try again later."; + +"location_sharing_locating_user_error_title" = "%@ could not access your location. Please try again later."; + +"location_sharing_invalid_authorization_error_title" = "%@ does not have permission to access your location. You can enable access in Settings > Location"; + +"location_sharing_invalid_authorization_not_now" = "Not now"; + +"location_sharing_invalid_authorization_settings" = "Settings"; + +"location_sharing_open_apple_maps" = "Open in Apple Maps"; + +"location_sharing_open_google_maps" = "Open in Google Maps"; + +"location_sharing_settings_header" = "Location sharing"; + +"location_sharing_settings_toggle_title" = "Enable location sharing"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 91fd9c0dd..29e931b56 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -115,6 +115,7 @@ internal enum Asset { internal static let peopleFloatingAction = ImageAsset(name: "people_floating_action") internal static let actionCamera = ImageAsset(name: "action_camera") internal static let actionFile = ImageAsset(name: "action_file") + internal static let actionLocation = ImageAsset(name: "action_location") internal static let actionMediaLibrary = ImageAsset(name: "action_media_library") internal static let actionPoll = ImageAsset(name: "action_poll") internal static let actionSticker = ImageAsset(name: "action_sticker") @@ -145,6 +146,9 @@ internal enum Asset { internal static let videoCall = ImageAsset(name: "video_call") internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon") internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") + internal static let locationMarkerIcon = ImageAsset(name: "location_marker_icon") + internal static let locationShareIcon = ImageAsset(name: "location_share_icon") + internal static let locationUserMarker = ImageAsset(name: "location_user_marker") internal static let pollCheckboxDefault = ImageAsset(name: "poll_checkbox_default") internal static let pollCheckboxSelected = ImageAsset(name: "poll_checkbox_selected") internal static let pollDeleteIcon = ImageAsset(name: "poll_delete_icon") diff --git a/Riot/Generated/MatrixKitStrings.swift b/Riot/Generated/MatrixKitStrings.swift index e5c1e7f31..b178f6d8d 100644 --- a/Riot/Generated/MatrixKitStrings.swift +++ b/Riot/Generated/MatrixKitStrings.swift @@ -695,6 +695,10 @@ public class MatrixKitL10n: NSObject { public static var messageReplyToSenderSentAnImage: String { return MatrixKitL10n.tr("message_reply_to_sender_sent_an_image") } + /// has shared their location. + public static var messageReplyToSenderSentTheirLocation: String { + return MatrixKitL10n.tr("message_reply_to_sender_sent_their_location") + } /// There are unsaved changes. Leaving will discard them. public static var messageUnsavedChanges: String { return MatrixKitL10n.tr("message_unsaved_changes") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 1f88225d6..049d94e44 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -35,9 +35,9 @@ public class VectorL10n: NSObject { public static func activeCallDetails(_ p1: String) -> String { return VectorL10n.tr("Vector", "active_call_details", p1) } - /// Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. - public static var analyticsPromptMessageNewUser: String { - return VectorL10n.tr("Vector", "analytics_prompt_message_new_user") + /// Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. + public static func analyticsPromptMessageNewUser(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_message_new_user", p1) } /// You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. public static var analyticsPromptMessageUpgrade: String { @@ -2191,6 +2191,54 @@ public class VectorL10n: NSObject { public static var less: String { return VectorL10n.tr("Vector", "less") } + /// Close + public static var locationSharingCloseAction: String { + return VectorL10n.tr("Vector", "location_sharing_close_action") + } + /// %@ does not have permission to access your location. You can enable access in Settings > Location + public static func locationSharingInvalidAuthorizationErrorTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_error_title", p1) + } + /// Not now + public static var locationSharingInvalidAuthorizationNotNow: String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_not_now") + } + /// Settings + public static var locationSharingInvalidAuthorizationSettings: String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings") + } + /// %@ could not load the map. Please try again later. + public static func locationSharingLoadingMapErrorTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_loading_map_error_title", p1) + } + /// %@ could not access your location. Please try again later. + public static func locationSharingLocatingUserErrorTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_locating_user_error_title", p1) + } + /// Open in Apple Maps + public static var locationSharingOpenAppleMaps: String { + return VectorL10n.tr("Vector", "location_sharing_open_apple_maps") + } + /// Open in Google Maps + public static var locationSharingOpenGoogleMaps: String { + return VectorL10n.tr("Vector", "location_sharing_open_google_maps") + } + /// Location sharing + public static var locationSharingSettingsHeader: String { + return VectorL10n.tr("Vector", "location_sharing_settings_header") + } + /// Enable location sharing + public static var locationSharingSettingsToggleTitle: String { + return VectorL10n.tr("Vector", "location_sharing_settings_toggle_title") + } + /// Share + public static var locationSharingShareAction: String { + return VectorL10n.tr("Vector", "location_sharing_share_action") + } + /// Location + public static var locationSharingTitle: String { + return VectorL10n.tr("Vector", "location_sharing_title") + } /// Got it public static var majorUpdateDoneAction: String { return VectorL10n.tr("Vector", "major_update_done_action") @@ -2291,6 +2339,10 @@ public class VectorL10n: NSObject { public static var off: String { return VectorL10n.tr("Vector", "off") } + /// OK + public static var ok: String { + return VectorL10n.tr("Vector", "ok") + } /// On public static var on: String { return VectorL10n.tr("Vector", "on") @@ -2443,10 +2495,6 @@ public class VectorL10n: NSObject { public static var pollEditFormPollQuestionOrTopic: String { return VectorL10n.tr("Vector", "poll_edit_form_poll_question_or_topic") } - /// OK - public static var pollEditFormPostFailureAction: String { - return VectorL10n.tr("Vector", "poll_edit_form_post_failure_action") - } /// Please try again public static var pollEditFormPostFailureSubtitle: String { return VectorL10n.tr("Vector", "poll_edit_form_post_failure_subtitle") @@ -2459,10 +2507,6 @@ public class VectorL10n: NSObject { public static var pollEditFormQuestionOrTopic: String { return VectorL10n.tr("Vector", "poll_edit_form_question_or_topic") } - /// OK - public static var pollTimelineNotClosedAction: String { - return VectorL10n.tr("Vector", "poll_timeline_not_closed_action") - } /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") @@ -2503,10 +2547,6 @@ public class VectorL10n: NSObject { public static func pollTimelineTotalVotesNotVoted(_ p1: Int) -> String { return VectorL10n.tr("Vector", "poll_timeline_total_votes_not_voted", p1) } - /// OK - public static var pollTimelineVoteNotRegisteredAction: String { - return VectorL10n.tr("Vector", "poll_timeline_vote_not_registered_action") - } /// Sorry, your vote was not registered, please try again public static var pollTimelineVoteNotRegisteredSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_vote_not_registered_subtitle") @@ -5139,9 +5179,9 @@ public class VectorL10n: NSObject { public static var spacesAddSpaceTitle: String { return VectorL10n.tr("Vector", "spaces_add_space_title") } - /// This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer. - public static var spacesComingSoonDetail: String { - return VectorL10n.tr("Vector", "spaces_coming_soon_detail") + /// This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with %@ on your computer. + public static func spacesComingSoonDetail(_ p1: String) -> String { + return VectorL10n.tr("Vector", "spaces_coming_soon_detail", p1) } /// Coming soon public static var spacesComingSoonTitle: String { diff --git a/Riot/Managers/AppInfo/AppInfo.swift b/Riot/Managers/AppInfo/AppInfo.swift index 457a7af87..fde62d75b 100644 --- a/Riot/Managers/AppInfo/AppInfo.swift +++ b/Riot/Managers/AppInfo/AppInfo.swift @@ -19,17 +19,14 @@ import Foundation /// Used to handle the application information @objcMembers final class AppInfo: NSObject { - + // MARK: - Constants /// Current application information static var current: AppInfo { - let appDisplayName = BuildSettings.bundleDisplayName - let buildInfo: BuildInfo = BuildInfo() - - return AppInfo(displayName: appDisplayName, + return AppInfo(displayName: self.bundleDisplayName, appVersion: AppVersion.current, - buildInfo: buildInfo) + buildInfo: BuildInfo()) } // MARK: - Properties @@ -52,4 +49,11 @@ final class AppInfo: NSObject { self.appVersion = appVersion self.buildInfo = buildInfo } + + private static var bundleDisplayName: String { + guard let bundleDisplayName = Bundle.app.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String else { + fatalError("CFBundleDisplayName should be defined") + } + return bundleDisplayName + } } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 3aa22a842..1813d8818 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -187,6 +187,9 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowPollsAction", defaultValue: false, storage: defaults) var roomScreenAllowPollsAction + + @UserDefault(key: "roomScreenAllowLocationAction", defaultValue: false, storage: defaults) + var roomScreenAllowLocationAction @UserDefault(key: "roomScreenShowsURLPreviews", defaultValue: true, storage: defaults) var roomScreenShowsURLPreviews diff --git a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift index ba0e9cf8a..c97176f31 100644 --- a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift +++ b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift @@ -55,7 +55,7 @@ class FindYourContactsFooterView: UIView, NibLoadable, Themable { button.layer.cornerRadius = 8 titleLabel.text = VectorL10n.findYourContactsTitle - messageLabel.text = VectorL10n.findYourContactsMessage(BuildSettings.bundleDisplayName) + messageLabel.text = VectorL10n.findYourContactsMessage(AppInfo.current.displayName) button.setTitle(VectorL10n.findYourContactsButtonTitle, for: .normal) footerLabel.text = VectorL10n.findYourContactsFooter } diff --git a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m index f9dcf9ea2..6e509e089 100644 --- a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m +++ b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m @@ -37,7 +37,7 @@ roomId = event.roomId; // Title is here the file name stored in event body - title = [event.content[@"body"] isKindOfClass:[NSString class]] ? event.content[@"body"] : nil; + title = [event.content[kMXMessageBodyKey] isKindOfClass:[NSString class]] ? event.content[kMXMessageBodyKey] : nil; // Check attachment if any if ([searchDataSource.eventFormatter isSupportedAttachment:event]) @@ -128,7 +128,7 @@ { MXEvent *event = searchResult.result; NSString *msgtype; - MXJSONModelSetString(msgtype, event.content[@"msgtype"]); + MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]); if ([msgtype isEqualToString:kMXMessageTypeImage]) { @@ -142,10 +142,6 @@ { return [UIImage imageNamed:@"file_video_icon"]; } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - // Not supported yet - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { return [UIImage imageNamed:@"file_doc_icon"]; diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings index f9e40406a..b7937f194 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings @@ -258,6 +258,7 @@ "message_reply_to_sender_sent_an_audio_file" = "sent an audio file."; "message_reply_to_sender_sent_a_voice_message" = "sent a voice message."; "message_reply_to_sender_sent_a_file" = "sent a file."; +"message_reply_to_sender_sent_their_location" = "has shared their location."; "message_reply_to_message_to_reply_to_prefix" = "In reply to"; // Room members diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m index f29b59ee7..942eeec58 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m @@ -2179,12 +2179,12 @@ if (event && event.eventType == MXEventTypeRoomMessage) { - NSString *msgtype = event.content[@"msgtype"]; + NSString *msgtype = event.content[kMXMessageTypeKey]; NSString* textMessage; if ([msgtype isEqualToString:kMXMessageTypeText]) { - textMessage = event.content[@"body"]; + textMessage = event.content[kMXMessageBodyKey]; } // Show a confirmation popup to the end user @@ -3668,9 +3668,6 @@ MXLogDebug(@"[MXKRoomVC] showAttachmentInCell on an unsent media"); } } - else if (selectedAttachment.type == MXKAttachmentTypeLocation) - { - } else if (selectedAttachment.type == MXKAttachmentTypeFile || selectedAttachment.type == MXKAttachmentTypeAudio) { // Start activity indicator as feedback on file selection. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h index 07e878161..287b8ef8a 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h @@ -33,7 +33,6 @@ typedef enum : NSUInteger { MXKAttachmentTypeAudio, MXKAttachmentTypeVoiceMessage, MXKAttachmentTypeVideo, - MXKAttachmentTypeLocation, MXKAttachmentTypeFile, MXKAttachmentTypeSticker diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m index e15fd70b3..634ca547f 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m @@ -91,7 +91,7 @@ NSString *const kMXKAttachmentFileNameBase = @"attatchment"; else { // Note: mxEvent.eventType is supposed to be MXEventTypeRoomMessage here. - NSString *msgtype = eventContent[@"msgtype"]; + NSString *msgtype = eventContent[kMXMessageTypeKey]; if ([msgtype isEqualToString:kMXMessageTypeImage]) { _type = MXKAttachmentTypeImage; @@ -109,12 +109,6 @@ NSString *const kMXKAttachmentFileNameBase = @"attatchment"; _type = MXKAttachmentTypeVideo; MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]); } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - // Not supported yet - // _type = MXKAttachmentTypeLocation; - return nil; - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { _type = MXKAttachmentTypeFile; @@ -125,7 +119,7 @@ NSString *const kMXKAttachmentFileNameBase = @"attatchment"; } } - MXJSONModelSetString(_originalFileName, eventContent[@"body"]); + MXJSONModelSetString(_originalFileName, eventContent[kMXMessageBodyKey]); MXJSONModelSetDictionary(_contentInfo, eventContent[@"info"]); MXJSONModelSetMXJSONModel(contentFile, MXEncryptedContentFile, eventContent[@"file"]); diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataWithAppendingMode.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataWithAppendingMode.m index 680086ddd..6790bddf7 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataWithAppendingMode.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataWithAppendingMode.m @@ -102,7 +102,7 @@ static NSAttributedString *messageSeparator = nil; return NO; } } - + // Add all components of the provided message for (MXKRoomBubbleComponent* component in cellData.bubbleComponents) { diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m index 565519ba9..e40ffbf02 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m @@ -115,7 +115,7 @@ return; } - NSString *messageType = self.event.content[@"msgtype"]; + NSString *messageType = self.event.content[kMXMessageTypeKey]; if (!messageType || !([messageType isEqualToString:kMXMessageTypeText] || [messageType isEqualToString:kMXMessageTypeNotice] || [messageType isEqualToString:kMXMessageTypeEmote])) { diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 8559fba2d..0510033a6 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -591,6 +591,25 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; success:(void (^)(NSString *eventId))success failure:(void (^)(NSError *error))failure; +/** + Send a location message to a room. + + 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 latitude the location's latitude + @param longitude the location's longitude + @param description an optional description + @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)sendLocationWithLatitude:(double)latitude + longitude:(double)longitude + description:(NSString *)description + success:(void (^)(NSString *))success + failure:(void (^)(NSError *))failure; + /** Send a generic non state event to a room. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 43ab3e623..c439bb54b 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -1914,6 +1914,29 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } } +- (void)sendLocationWithLatitude:(double)latitude + longitude:(double)longitude + description:(NSString *)description + success:(void (^)(NSString *))success + failure:(void (^)(NSError *))failure +{ + __block MXEvent *localEchoEvent = nil; + + // Make the request to the homeserver + [_room sendLocationWithLatitude:latitude + longitude:longitude + description:description + localEcho:&localEchoEvent + success:success failure:failure]; + + if (localEchoEvent) + { + // Make the data source digest this fake local echo message + [self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards]; + [self processQueuedEvents:nil]; + } +} + - (void)sendEventOfType:(MXEventTypeString)eventTypeString content:(NSDictionary*)msgContent success:(void (^)(NSString *eventId))success failure:(void (^)(NSError *error))failure { __block MXEvent *localEchoEvent = nil; @@ -1951,7 +1974,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { else if ([event.type isEqualToString:kMXEventTypeStringRoomMessage]) { // And retry the send the message according to its type - NSString *msgType = event.content[@"msgtype"]; + NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeText] || [msgType isEqualToString:kMXMessageTypeEmote]) { // Resend the Matrix event by reusing the existing echo @@ -2712,7 +2735,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { return NO; } - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; if (messageType == nil || [messageType isEqualToString:@"m.bad.encrypted"]) { return NO; } @@ -3928,7 +3951,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { if ([self canPerformActionOnEvent:event]) { - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { @@ -3971,7 +3994,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { MXEvent *event = [self eventWithEventId:eventId]; BOOL isRoomMessage = event.eventType == MXEventTypeRoomMessage; - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; return isRoomMessage && ([messageType isEqualToString:kMXMessageTypeText] || [messageType isEqualToString:kMXMessageTypeEmote]) @@ -3992,7 +4015,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } else { - editableTextMessage = event.content[@"body"]; + editableTextMessage = event.content[kMXMessageBodyKey]; } return editableTextMessage; @@ -4109,7 +4132,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { NSString *sanitizedText = [self sanitizedMessageText:text]; NSString *formattedText = [self htmlMessageFromSanitizedText:sanitizedText]; - NSString *eventBody = event.content[@"body"]; + NSString *eventBody = event.content[kMXMessageBodyKey]; NSString *eventFormattedBody = event.content[@"formatted_body"]; if (![sanitizedText isEqualToString:eventBody] && (!eventFormattedBody || ![formattedText isEqualToString:eventFormattedBody])) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m index f116c466b..b592db0d6 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m @@ -45,6 +45,11 @@ return [MatrixKitL10n messageReplyToSenderSentAFile]; } +- (NSString *)senderSentTheirLocation +{ + return [MatrixKitL10n messageReplyToSenderSentTheirLocation]; +} + - (NSString *)messageToReplyToPrefix { return [MatrixKitL10n messageReplyToMessageToReplyToPrefix]; diff --git a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m index 246dbd87d..d1daa83ad 100644 --- a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m +++ b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m @@ -56,7 +56,7 @@ date = [searchDataSource.eventFormatter dateStringFromEvent:searchResult.result withTime:YES]; // Code from [MXEventFormatter stringFromEvent] for the particular case of a text message - message = [searchResult.result.content[@"body"] isKindOfClass:[NSString class]] ? searchResult.result.content[@"body"] : nil; + message = [searchResult.result.content[kMXMessageBodyKey] isKindOfClass:[NSString class]] ? searchResult.result.content[kMXMessageBodyKey] : nil; } return self; } diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 9177cc5be..46abe46ab 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -175,10 +175,6 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; { isSupportedAttachment = hasUrl || hasFile; } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - // Not supported yet - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { isSupportedAttachment = hasUrl || hasFile; @@ -1252,7 +1248,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else { NSString *msgtype; - MXJSONModelSetString(msgtype, event.content[@"msgtype"]); + MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]); NSString *body; BOOL isHTML = NO; @@ -1267,12 +1263,12 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else if (eventThreadIdentifier) { isHTML = YES; - MXJSONModelSetString(body, event.content[@"body"]); + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); MXEvent *threadRootEvent = [mxSession.store eventWithEventId:eventThreadIdentifier inRoom:event.roomId]; NSString *threadRootEventContent; - MXJSONModelSetString(threadRootEventContent, threadRootEvent.content[@"body"]); + MXJSONModelSetString(threadRootEventContent, threadRootEvent.content[kMXMessageBodyKey]); body = [NSString stringWithFormat:@"
In reply to %@
%@
%@", [MXTools permalinkToEvent:eventThreadIdentifier inRoom:event.roomId], [MXTools permalinkToUserWithUserId:threadRootEvent.sender], @@ -1283,7 +1279,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; } else { - MXJSONModelSetString(body, event.content[@"body"]); + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); } if (body) @@ -1333,23 +1329,6 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; *error = MXKEventFormatterErrorUnsupported; } } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - body = body? body : [MatrixKitL10n noticeLocationAttachment]; - if (![self isSupportedAttachment:event]) - { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment %@", event.description); - if (_isForSubtitle || !_settings.showUnsupportedEventsInRoomHistory) - { - body = [MatrixKitL10n noticeInvalidAttachment]; - } - else - { - body = [MatrixKitL10n noticeUnsupportedAttachment:event.description]; - } - *error = MXKEventFormatterErrorUnsupported; - } - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { body = body? body : [MatrixKitL10n noticeFileAttachment]; @@ -1582,7 +1561,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else { NSString *body; - MXJSONModelSetString(body, event.content[@"body"]); + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); // Check sticker validity if (![self isSupportedAttachment:event]) @@ -2000,7 +1979,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; if (event.eventType == MXEventTypeRoomMessage) { - NSString *msgtype = event.content[@"msgtype"]; + NSString *msgtype = event.content[kMXMessageTypeKey]; if ([msgtype isEqualToString:kMXMessageTypeEmote] == NO) { NSString *senderDisplayName = [self senderDisplayNameForEvent:event withRoomState:roomState]; @@ -2121,7 +2100,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else if (!_isForSubtitle && event.eventType == MXEventTypeRoomMessage && (_emojiOnlyTextFont || _singleEmojiTextFont)) { NSString *message; - MXJSONModelSetString(message, event.content[@"body"]); + MXJSONModelSetString(message, event.content[kMXMessageBodyKey]); if (_emojiOnlyTextFont && [MXKTools isEmojiOnlyString:message]) { diff --git a/Riot/Modules/People/InviteFriendsPresenter.swift b/Riot/Modules/People/InviteFriendsPresenter.swift index ad8995746..8f6f9f8b6 100644 --- a/Riot/Modules/People/InviteFriendsPresenter.swift +++ b/Riot/Modules/People/InviteFriendsPresenter.swift @@ -61,7 +61,7 @@ final class InviteFriendsPresenter: NSObject { private func buildShareText(with userId: String) -> String { let userMatrixToLink: String = MXTools.permalinkToUser(withUserId: userId) - return VectorL10n.inviteFriendsShareText(BuildSettings.bundleDisplayName, userMatrixToLink) + return VectorL10n.inviteFriendsShareText(AppInfo.current.displayName, userMatrixToLink) } private func present(_ viewController: UIViewController, animated: Bool) { diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index ca0058be5..706776efb 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -32,7 +32,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) RoomBubbleCellDataTagCall, RoomBubbleCellDataTagGroupCall, RoomBubbleCellDataTagRoomCreationIntro, - RoomBubbleCellDataTagPoll + RoomBubbleCellDataTagPoll, + RoomBubbleCellDataTagLocation }; /** diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 4c9e9a4e2..81867be76 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -174,8 +174,17 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat self.displayTimestampForSelectedComponentOnLeftWhenPossible = NO; } } - } + break; + } + case MXEventTypeRoomMessage: + { + if (event.location) { + self.tag = RoomBubbleCellDataTagLocation; + self.collapsable = NO; + self.collapsed = NO; + } + } default: break; } @@ -273,6 +282,11 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat return NO; } + if (self.tag == RoomBubbleCellDataTagLocation) + { + return NO; + } + return [super hasNoDisplay]; } @@ -845,6 +859,9 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat case RoomBubbleCellDataTagPoll: shouldAddEvent = NO; break; + case RoomBubbleCellDataTagLocation: + shouldAddEvent = NO; + break; default: break; } @@ -857,7 +874,12 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat { case MXEventTypeRoomMessage: { - NSString *messageType = event.content[@"msgtype"]; + if (event.location) { + shouldAddEvent = NO; + break; + } + + NSString *messageType = event.content[kMXMessageTypeKey]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { @@ -991,7 +1013,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat break; case MXEventTypeRoomMessage: { - NSString *msgType = event.content[@"msgtype"]; + NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { @@ -1044,7 +1066,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat { NSString *mediaName = [self accessibilityLabelForAttachmentType:self.attachment.type]; - MXJSONModelSetString(accessibilityLabel, self.events.firstObject.content[@"body"]); + MXJSONModelSetString(accessibilityLabel, self.events.firstObject.content[kMXMessageBodyKey]); if (accessibilityLabel) { accessibilityLabel = [NSString stringWithFormat:@"%@ %@", mediaName, accessibilityLabel]; @@ -1075,9 +1097,6 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat case MXKAttachmentTypeVideo: accessibilityLabel = [VectorL10n mediaTypeAccessibilityVideo]; break; - case MXKAttachmentTypeLocation: - accessibilityLabel = [VectorL10n mediaTypeAccessibilityLocation]; - break; case MXKAttachmentTypeFile: accessibilityLabel = [VectorL10n mediaTypeAccessibilityFile]; break; diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index f160a851f..1f6b51137 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -836,7 +836,7 @@ const CGFloat kTypingCellHeight = 24; break; case MXEventTypeRoomMessage: { - NSString *msgType = event.content[@"msgtype"]; + NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { diff --git a/Riot/Modules/Room/Location/LocationUserMarkerView.swift b/Riot/Modules/Room/Location/LocationUserMarkerView.swift new file mode 100644 index 000000000..995d1ae56 --- /dev/null +++ b/Riot/Modules/Room/Location/LocationUserMarkerView.swift @@ -0,0 +1,33 @@ +// +// Copyright 2021 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 +import Mapbox + +class LocationUserMarkerView: MGLAnnotationView, NibLoadable { + + @IBOutlet private var avatarView: UserAvatarView! + + override func awakeFromNib() { + super.awakeFromNib() + translatesAutoresizingMaskIntoConstraints = false + } + + func setAvatarData(_ avatarData: AvatarViewDataProtocol) { + avatarView.fill(with: avatarData) + } +} diff --git a/Riot/Modules/Room/Location/LocationUserMarkerView.xib b/Riot/Modules/Room/Location/LocationUserMarkerView.xib new file mode 100644 index 000000000..26495f925 --- /dev/null +++ b/Riot/Modules/Room/Location/LocationUserMarkerView.xib @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift new file mode 100644 index 000000000..82cebe7cd --- /dev/null +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -0,0 +1,114 @@ +// +// Copyright 2021 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 +import Mapbox +import Keys + +class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegate { + + // MARK: - Constants + + private struct Constants { + static let mapHeight: CGFloat = 300.0 + static let mapTilerKey = RiotKeys().mapTilerAPIKey + static let mapZoomLevel = 15.0 + static let cellBorderRadius: CGFloat = 1.0 + static let cellCornerRadius: CGFloat = 8.0 + } + + // MARK: - Properties + // MARK: Private + + @IBOutlet private var descriptionContainerView: UIView! + @IBOutlet private var descriptionLabel: UILabel! + + private var mapView: MGLMapView! + private var annotationView: LocationUserMarkerView? + + // MARK: Public + + var locationDescription: String? { + get { + descriptionLabel.text + } + set { + descriptionLabel.text = newValue + descriptionContainerView.isHidden = (newValue?.count ?? 0 == 0) + } + } + + override func awakeFromNib() { + super.awakeFromNib() + + mapView = MGLMapView(frame: .zero, styleURL: BuildSettings.tileServerMapURL) + mapView.delegate = self + mapView.logoView.isHidden = true + mapView.attributionButton.isHidden = true + mapView.isUserInteractionEnabled = false + + mapView.translatesAutoresizingMaskIntoConstraints = false + mapView.addConstraint(mapView.heightAnchor.constraint(equalToConstant: Constants.mapHeight)) + vc_addSubViewMatchingParent(mapView) + sendSubviewToBack(mapView) + + clipsToBounds = true + layer.borderWidth = Constants.cellBorderRadius + layer.cornerRadius = Constants.cellCornerRadius + } + + // MARK: - Public + + public func displayLocation(_ location: CLLocationCoordinate2D, + userIdentifier: String, + userDisplayName: String, + userAvatarURLString: String?, + mediaManager: MXMediaManager) { + + annotationView = LocationUserMarkerView.loadFromNib() + + annotationView?.setAvatarData(AvatarViewData(matrixItemId: userIdentifier, + displayName: userDisplayName, + avatarUrl: userAvatarURLString, + mediaManager: mediaManager, + fallbackImage: .matrixItem(userIdentifier, userDisplayName))) + + if let annotations = mapView.annotations { + mapView.removeAnnotations(annotations) + } + + mapView.setCenter(location, zoomLevel: Constants.mapZoomLevel, animated: false) + + let pointAnnotation = MGLPointAnnotation() + pointAnnotation.coordinate = location + mapView.addAnnotation(pointAnnotation) + } + + // MARK: - Themable + + func update(theme: Theme) { + descriptionLabel.textColor = theme.colors.primaryContent + descriptionLabel.font = theme.fonts.footnote + layer.borderColor = theme.colors.quinaryContent.cgColor + } + + // MARK: - MGLMapViewDelegate + + func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { + return annotationView + } +} diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib new file mode 100644 index 000000000..8beacacbe --- /dev/null +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 7f860168f..fec16dd93 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -30,8 +30,6 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private let activityIndicatorPresenter: ActivityIndicatorPresenterType private var selectedEventId: String? - private var pollEditFormCoordinator: PollEditFormCoordinator? - private var roomDataSourceManager: MXKRoomDataSourceManager { return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) } @@ -198,6 +196,56 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { completion?() } + + private func startLocationCoordinatorWithEvent(_ event: MXEvent? = nil, bubbleData: MXKRoomBubbleCellDataStoring? = nil) { + guard #available(iOS 14.0, *) else { + return + } + + guard let navigationRouter = self.navigationRouter, + let mediaManager = mxSession?.mediaManager, + let user = mxSession?.myUser else { + MXLog.error("[RoomCoordinator] Invalid location sharing coordinator parameters. Returning.") + return + } + + var avatarData: AvatarInputProtocol + if event != nil, let bubbleData = bubbleData { + avatarData = AvatarInput(mxContentUri: bubbleData.senderAvatarUrl, + matrixItemId: bubbleData.senderId, + displayName: bubbleData.senderDisplayName) + } else { + avatarData = AvatarInput(mxContentUri: user.avatarUrl, + matrixItemId: user.userId, + displayName: user.displayname) + } + + var location: CLLocationCoordinate2D? + if let locationContent = event?.location { + location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) + } + + let parameters = LocationSharingCoordinatorParameters(roomDataSource: roomViewController.roomDataSource, + mediaManager: mediaManager, + avatarData: avatarData, + location: location) + + let coordinator = LocationSharingCoordinator(parameters: parameters) + + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { + return + } + + self.navigationRouter?.dismissModule(animated: true, completion: nil) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + + navigationRouter.present(coordinator, animated: true) + coordinator.start() + } } // MARK: - RoomIdentifiable @@ -261,10 +309,30 @@ extension RoomCoordinator: RoomViewControllerDelegate { return } - let parameters = PollEditFormCoordinatorParameters(navigationRouter: self.navigationRouter, room: roomViewController.roomDataSource.room) - pollEditFormCoordinator = PollEditFormCoordinator(parameters: parameters) + let parameters = PollEditFormCoordinatorParameters(room: roomViewController.roomDataSource.room) + let coordinator = PollEditFormCoordinator(parameters: parameters) - pollEditFormCoordinator?.start() + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { + return + } + + self.navigationRouter?.dismissModule(animated: true, completion: nil) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + + navigationRouter?.present(coordinator, animated: true) + coordinator.start() + } + + func roomViewControllerDidRequestLocationSharingFormPresentation(_ roomViewController: RoomViewController) { + startLocationCoordinatorWithEvent() + } + + func roomViewController(_ roomViewController: RoomViewController, didRequestLocationPresentationFor event: MXEvent, bubbleData: MXKRoomBubbleCellDataStoring) { + startLocationCoordinatorWithEvent(event, bubbleData: bubbleData) } func roomViewController(_ roomViewController: RoomViewController, canEndPollWithEventIdentifier eventIdentifier: String) -> Bool { diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 74941c31e..f3b9f6fa6 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -183,6 +183,24 @@ handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; */ - (void)roomViewControllerDidRequestPollCreationFormPresentation:(RoomViewController *)roomViewController; +/** + Ask the coordinator to invoke the location sharing form coordinator. + + @param roomViewController the `RoomViewController` instance. + */ +- (void)roomViewControllerDidRequestLocationSharingFormPresentation:(RoomViewController *)roomViewController; + +/** + Ask the coordinator to invoke the location sharing form coordinator. + + @param roomViewController the `RoomViewController` instance. + @param event the event containing location information + @param bubbleData the bubble data containing sender details + */ +- (void)roomViewController:(RoomViewController *)roomViewController +didRequestLocationPresentationForEvent:(MXEvent *)event + bubbleData:(id)bubbleData; + - (BOOL)roomViewController:(RoomViewController *)roomViewController canEndPollWithEventIdentifier:(NSString *)eventIdentifier; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index ac57d3a78..10af1f3bc 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -422,6 +422,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.bubblesTableView registerClass:PollBubbleCell.class forCellReuseIdentifier:PollBubbleCell.defaultReuseIdentifier]; [self.bubblesTableView registerClass:PollWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:PollWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; [self.bubblesTableView registerClass:PollWithPaginationTitleBubbleCell.class forCellReuseIdentifier:PollWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + + [self.bubblesTableView registerClass:LocationBubbleCell.class forCellReuseIdentifier:LocationBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:LocationWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:LocationWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:LocationWithPaginationTitleBubbleCell.class forCellReuseIdentifier:LocationWithPaginationTitleBubbleCell.defaultReuseIdentifier]; [self vc_removeBackTitle]; @@ -2032,6 +2036,16 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self]; }]]; } + if (RiotSettings.shared.roomScreenAllowLocationAction) + { + [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_location"] andAction:^{ + MXStrongifyAndReturnIfNil(self); + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } + [self.delegate roomViewControllerDidRequestLocationSharingFormPresentation:self]; + }]]; + } if (RiotSettings.shared.roomScreenAllowCameraAction) { [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_camera"] andAction:^{ @@ -2731,6 +2745,21 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; cellViewClass = PollBubbleCell.class; } } + else if (bubbleData.tag == RoomBubbleCellDataTagLocation) + { + if (bubbleData.isPaginationFirstBubble) + { + cellViewClass = LocationWithPaginationTitleBubbleCell.class; + } + else if (bubbleData.shouldHideSenderInformation) + { + cellViewClass = LocationWithoutSenderInfoBubbleCell.class; + } + else + { + cellViewClass = LocationBubbleCell.class; + } + } else if (bubbleData.isIncoming) { if (bubbleData.isAttachmentWithThumbnail) @@ -2931,7 +2960,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } else { - [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES]; + if (tappedEvent.location) { + [_delegate roomViewController:self didRequestLocationPresentationForEvent:tappedEvent bubbleData:bubbleData]; + } else { + [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES]; + } } } } @@ -3248,7 +3281,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } - if (selectedEvent.sentState == MXEventSentStateSent && selectedEvent.eventType != MXEventTypePollStart) + if (selectedEvent.sentState == MXEventSentStateSent && + selectedEvent.eventType != MXEventTypePollStart && + !selectedEvent.location) { [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] style:UIAlertActionStyleDefault @@ -6101,7 +6136,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; switch (event.eventType) { case MXEventTypeRoomMessage: { - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib index 56c26c56c..adbd1b79a 100644 --- a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib @@ -140,14 +140,14 @@ - + - + diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift new file mode 100644 index 000000000..2795d2647 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift @@ -0,0 +1,63 @@ +// +// Copyright 2021 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 + +class LocationBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { + + private var locationView: RoomTimelineLocationView! + + override func render(_ cellData: MXKCellData!) { + super.render(cellData) + + guard #available(iOS 14.0, *), + let bubbleData = cellData as? RoomBubbleCellData, + let event = bubbleData.events.last, + event.eventType == __MXEventType.roomMessage, + let locationContent = event.location + else { + return + } + + locationView.update(theme: ThemeService.shared().theme) + locationView.locationDescription = locationContent.locationDescription + + let location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) + + locationView.displayLocation(location, + userIdentifier: bubbleData.senderId, + userDisplayName: bubbleData.senderDisplayName, + userAvatarURLString: bubbleData.senderAvatarUrl, + mediaManager: bubbleData.mxSession.mediaManager) + } + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.backgroundColor = .clear + bubbleCellContentView?.showSenderInfo = true + bubbleCellContentView?.showPaginationTitle = false + + guard #available(iOS 14.0, *), + let contentView = bubbleCellContentView?.innerContentView else { + return + } + + locationView = RoomTimelineLocationView.loadFromNib() + + contentView.vc_addSubViewMatchingParent(locationView) + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift new file mode 100644 index 000000000..e3f6dd84f --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 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 + +class LocationWithPaginationTitleBubbleCell: LocationBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift new file mode 100644 index 000000000..1329221dd --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 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 + +class LocationWithoutSenderInfoBubbleCell: LocationBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift index 0c521b1b8..9b3f85cec 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift @@ -35,7 +35,7 @@ struct VoiceMessageAudioConverter { static func mediaDurationAt(_ sourceURL: URL, completion: @escaping (Result) -> Void) { FFprobeKit.getMediaInformationAsync(sourceURL.path) { session in - guard let session = session as? MediaInformationSession else { + guard let session = session else { completion(.failure(.generic("Invalid session"))) return } @@ -46,14 +46,14 @@ struct VoiceMessageAudioConverter { } DispatchQueue.main.async { - if returnCode.isSuccess() { + if returnCode.isValueSuccess() { let mediaInfo = session.getMediaInformation() if let duration = try? TimeInterval(value: mediaInfo?.getDuration() ?? "0") { completion(.success(duration)) } else { completion(.failure(.generic("Failed to get media duration"))) } - } else if returnCode.isCancel() { + } else if returnCode.isValueCancel() { completion(.failure(.cancelled)) } else { completion(.failure(.generic(String(returnCode.getValue())))) @@ -82,9 +82,9 @@ struct VoiceMessageAudioConverter { } DispatchQueue.main.async { - if returnCode.isSuccess() { + if returnCode.isValueSuccess() { completion(.success(())) - } else if returnCode.isCancel() { + } else if returnCode.isValueCancel() { completion(.failure(.cancelled)) } else { completion(.failure(.generic(String(returnCode.getValue())))) diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 9aeaa77b7..2970c51f7 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -47,10 +47,11 @@ NSString* const kSettingsViewControllerPhoneBookCountryCellId = @"kSettingsViewControllerPhoneBookCountryCellId"; -enum +typedef NS_ENUM(NSUInteger, SECTION_TAG) { SECTION_TAG_SIGN_OUT = 0, SECTION_TAG_USER_SETTINGS, + SECTION_TAG_LOCATION_SHARING, SECTION_TAG_SENDING_MEDIA, SECTION_TAG_LINKS, SECTION_TAG_SECURITY, @@ -69,7 +70,7 @@ enum SECTION_TAG_DEACTIVATE_ACCOUNT }; -enum +typedef NS_ENUM(NSUInteger, USER_SETTINGS_INDEX) { USER_SETTINGS_PROFILE_PICTURE_INDEX = 0, USER_SETTINGS_DISPLAYNAME_INDEX, @@ -80,24 +81,29 @@ enum USER_SETTINGS_ADD_PHONENUMBER_INDEX }; -enum +typedef NS_ENUM(NSUInteger, USER_SETTINGS_OFFSET) { USER_SETTINGS_EMAILS_OFFSET = 2000, USER_SETTINGS_PHONENUMBERS_OFFSET = 1000 }; -enum +typedef NS_ENUM(NSUInteger, LOCATION_SHARING) +{ + LOCATION_SHARING_ENABLED +}; + +typedef NS_ENUM(NSUInteger, SENDING_MEDIA) { SENDING_MEDIA_CONFIRM_SIZE = 0 }; -enum +typedef NS_ENUM(NSUInteger, LINKS_SHOW_URL_PREVIEWS) { LINKS_SHOW_URL_PREVIEWS_INDEX = 0, LINKS_SHOW_URL_PREVIEWS_DESCRIPTION_INDEX }; -enum +typedef NS_ENUM(NSUInteger, NOTIFICATION_SETTINGS) { NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX = 0, NOTIFICATION_SETTINGS_SYSTEM_SETTINGS, @@ -109,33 +115,34 @@ enum NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX, }; -enum +typedef NS_ENUM(NSUInteger, CALLS_ENABLE_STUN_SERVER) { CALLS_ENABLE_STUN_SERVER_FALLBACK_INDEX = 0 }; -enum +typedef NS_ENUM(NSUInteger, INTEGRATIONS) { INTEGRATIONS_INDEX }; -enum { +typedef NS_ENUM(NSUInteger, LOCAL_CONTACTS) +{ LOCAL_CONTACTS_SYNC_INDEX, LOCAL_CONTACTS_PHONEBOOK_COUNTRY_INDEX }; -enum +typedef NS_ENUM(NSUInteger, USER_INTERFACE) { USER_INTERFACE_LANGUAGE_INDEX = 0, USER_INTERFACE_THEME_INDEX }; -enum +typedef NS_ENUM(NSUInteger, IDENTITY_SERVER) { IDENTITY_SERVER_INDEX }; -enum +typedef NS_ENUM(NSUInteger, ADVANCED) { ADVANCED_SHOW_NSFW_ROOMS_INDEX = 0, ADVANCED_CRASH_REPORT_INDEX, @@ -145,7 +152,7 @@ enum ADVANCED_REPORT_BUG_INDEX, }; -enum +typedef NS_ENUM(NSUInteger, ABOUT) { ABOUT_COPYRIGHT_INDEX = 0, ABOUT_TERM_CONDITIONS_INDEX, @@ -159,7 +166,7 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) LABS_ENABLE_POLLS }; -enum +typedef NS_ENUM(NSUInteger, SECURITY) { SECURITY_BUTTON_INDEX = 0, }; @@ -374,6 +381,14 @@ TableViewSectionsDelegate> sectionUserSettings.headerTitle = [VectorL10n settingsUserSettings]; [tmpSections addObject:sectionUserSettings]; + if (BuildSettings.locationSharingEnabled) + { + Section *sectionLocationSharing = [Section sectionWithTag:SECTION_TAG_LOCATION_SHARING]; + [sectionLocationSharing addRowWithTag:LOCATION_SHARING_ENABLED]; + sectionLocationSharing.headerTitle = VectorL10n.locationSharingSettingsHeader.uppercaseString; + [tmpSections addObject:sectionLocationSharing]; + } + if (BuildSettings.settingsScreenShowConfirmMediaSize) { Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_SENDING_MEDIA]; @@ -1942,6 +1957,21 @@ TableViewSectionsDelegate> cell = passwordCell; } } + else if (section == SECTION_TAG_LOCATION_SHARING) + { + if (row == LOCATION_SHARING_ENABLED) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = VectorL10n.locationSharingSettingsToggleTitle; + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenAllowLocationAction; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleLocationSharing:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + } else if (section == SECTION_TAG_SENDING_MEDIA) { if (row == SENDING_MEDIA_CONFIRM_SIZE) @@ -2964,6 +2994,11 @@ TableViewSectionsDelegate> } } +- (void)toggleLocationSharing:(UISwitch *)sender +{ + RiotSettings.shared.roomScreenAllowLocationAction = sender.on; +} + - (void)toggleConfirmMediaSize:(UISwitch *)sender { RiotSettings.shared.showMediaCompressionPrompt = sender.on; diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift index d05076758..9356ee904 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift @@ -154,7 +154,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { // MARK: - Actions @objc private func onAddParticipantButtonPressed() { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesInvitesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesInvitesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } private func cancelButtonAction() { @@ -184,11 +184,11 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { override func roomMemberDetailsViewController(_ roomMemberDetailsViewController: MXKRoomMemberDetailsViewController!, startChatWithMemberId matrixId: String!, completion: (() -> Void)!) { completion() - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } override func roomMemberDetailsViewController(_ roomMemberDetailsViewController: MXKRoomMemberDetailsViewController!, placeVoipCallWithMemberId matrixId: String!, andVideo isVideoCall: Bool) { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift index fc04d7473..5c13e08f8 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift @@ -225,7 +225,7 @@ final class SpaceExploreRoomViewController: UIViewController { } @objc private func addRoomAction(semder: UIView) { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } // MARK: - UISearchBarDelegate diff --git a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift index 30325e704..2c61559c9 100644 --- a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift +++ b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift @@ -50,7 +50,7 @@ final class InviteFriendsHeaderView: UIView, NibLoadable, Themable { override func awakeFromNib() { super.awakeFromNib() - button.setTitle(VectorL10n.inviteFriendsAction(BuildSettings.bundleDisplayName), for: .normal) + button.setTitle(VectorL10n.inviteFriendsAction(AppInfo.current.displayName), for: .normal) button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) button.layer.cornerRadius = 8 button.layer.borderWidth = 2 diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index ac72eb2eb..37342d973 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -489,15 +489,20 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { @available(iOS 14.0, *) private func presentAnalyticsPrompt(with session: MXSession) { - let parameters = AnalyticsPromptCoordinatorParameters(session: session, navigationRouter: navigationRouter) + let parameters = AnalyticsPromptCoordinatorParameters(session: session) let coordinator = AnalyticsPromptCoordinator(parameters: parameters) + coordinator.completion = { [weak self, weak coordinator] in guard let self = self, let coordinator = coordinator else { return } + + self.navigationRouter.dismissModule(animated: true, completion: nil) self.remove(childCoordinator: coordinator) } - coordinator.start() add(childCoordinator: coordinator) + + navigationRouter.present(coordinator, animated: true) + coordinator.start() } // MARK: UserSessions management diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist index 5bcda43e6..9d95fe929 100644 --- a/Riot/SupportingFiles/Info.plist +++ b/Riot/SupportingFiles/Info.plist @@ -65,6 +65,8 @@ The photo library is used to send photos and videos. NSSiriUsageDescription Siri is used to perform calls even from the lock screen. + NSLocationWhenInUseUsageDescription + When you share your location to people, Element needs access to show them a map. UIBackgroundModes audio diff --git a/Riot/target.yml b/Riot/target.yml index aaac28a9b..81a912fbc 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -35,6 +35,7 @@ targets: - target: SiriIntents - target: RiotNSE - target: DesignKit + - package: Mapbox configFiles: Debug: Debug.xcconfig diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 2dfaec7f3..c77a9dd86 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -385,8 +385,8 @@ class NotificationService: UNNotificationServiceExtension { } } - let msgType = event.content["msgtype"] as? String - let messageContent = event.content["body"] as? String + let msgType = event.content[kMXMessageTypeKey] as? String + let messageContent = event.content[kMXMessageBodyKey] as? String let isReply = event.isReply() if isReply { @@ -401,6 +401,11 @@ class NotificationService: UNNotificationServiceExtension { break } + if event.location != nil { + notificationBody = NSString.localizedUserNotificationString(forKey: "LOCATION_FROM_USER", arguments: [eventSenderName]) + break + } + switch msgType { case kMXMessageTypeEmote: notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName, messageContent as Any]) diff --git a/RiotSwiftUI/Common.xcconfig b/RiotSwiftUI/Common.xcconfig index 9c6902651..37b97f356 100644 --- a/RiotSwiftUI/Common.xcconfig +++ b/RiotSwiftUI/Common.xcconfig @@ -27,4 +27,6 @@ INFOPLIST_FILE = RiotSwiftUI/Info.plist SKIP_INSTALL = YES +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h + SWIFT_OBJC_INTERFACE_HEADER_NAME = GeneratedInterface-Swift.h diff --git a/RiotSwiftUI/Info.plist b/RiotSwiftUI/Info.plist index 0a5393324..859c68836 100644 --- a/RiotSwiftUI/Info.plist +++ b/RiotSwiftUI/Info.plist @@ -20,5 +20,9 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + CFBundleDisplayName + RiotSwiftUI + NSLocationWhenInUseUsageDescription + When you share your location to people, Element needs access to show them a map. diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift index add81e84b..b9678fdbd 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -66,7 +66,7 @@ extension AnalyticsPromptType { var message: String { switch self { case .newUser: - return VectorL10n.analyticsPromptMessageNewUser + return VectorL10n.analyticsPromptMessageNewUser(AppInfo.current.displayName) case .upgrade: return VectorL10n.analyticsPromptMessageUpgrade } diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift index 208b289d7..c9e5f44b4 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift @@ -21,11 +21,9 @@ import SwiftUI struct AnalyticsPromptCoordinatorParameters { /// The session to use if analytics are enabled. let session: MXSession - /// The navigation router used to display the prompt. - let navigationRouter: NavigationRouterType } -final class AnalyticsPromptCoordinator: Coordinator { +final class AnalyticsPromptCoordinator: Coordinator, Presentable { // MARK: - Properties @@ -78,8 +76,6 @@ final class AnalyticsPromptCoordinator: Coordinator { MXLog.debug("[AnalyticsPromptCoordinator] did start.") - parameters.navigationRouter.present(toPresentable(), animated: true) - analyticsPromptViewModel.completion = { [weak self] result in MXLog.debug("[AnalyticsPromptCoordinator] AnalyticsPromptViewModel did complete with result: \(result).") @@ -88,11 +84,9 @@ final class AnalyticsPromptCoordinator: Coordinator { switch result { case .enable: Analytics.shared.optIn(with: self.parameters.session) - self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) self.completion?() case .disable: Analytics.shared.optOut() - self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) self.completion?() } } diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift index ada017da6..b8a38a117 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift @@ -49,7 +49,7 @@ class AnalyticsPromptUITests: MockScreenTest { switch promptType { case .newUser: XCTAssertEqual(enableButton.label, VectorL10n.enable) - XCTAssertEqual(disableButton.label, VectorL10n.cancel) + XCTAssertEqual(disableButton.label, VectorL10n.locationSharingInvalidAuthorizationNotNow) case .upgrade: XCTAssertEqual(enableButton.label, VectorL10n.analyticsPromptYes) XCTAssertEqual(disableButton.label, VectorL10n.analyticsPromptStop) diff --git a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift index 821c71ef0..1b083d1e0 100644 --- a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift +++ b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift @@ -21,14 +21,17 @@ import SwiftUI /// A modifier for showing the activity indicator centered over a view. struct ActivityIndicatorModifier: ViewModifier { var show: Bool - + @ViewBuilder func body(content: Content) -> some View { + content + .overlay(activityIndicator, alignment: .center) + } + + @ViewBuilder + private var activityIndicator: some View { if show { - content - .overlay(ActivityIndicator(), alignment: .center) - } else { - content + ActivityIndicator() } } } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 63a4f1bac..5400da148 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -20,6 +20,7 @@ import Foundation @available(iOS 14.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ + MockLocationSharingScreenState.self, MockAnalyticsPromptScreenState.self, MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift new file mode 100644 index 000000000..931c6a3d7 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -0,0 +1,120 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit +import SwiftUI +import Keys + +struct LocationSharingCoordinatorParameters { + let roomDataSource: MXKRoomDataSource + let mediaManager: MXMediaManager + let avatarData: AvatarInputProtocol + let location: CLLocationCoordinate2D? +} + +final class LocationSharingCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: LocationSharingCoordinatorParameters + private let locationSharingHostingController: UIViewController + private var _locationSharingViewModel: Any? = nil + + @available(iOS 14.0, *) + fileprivate var locationSharingViewModel: LocationSharingViewModel { + return _locationSharingViewModel as! LocationSharingViewModel + } + + // MARK: Public + + var childCoordinators: [Coordinator] = [] + + var completion: (() -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: LocationSharingCoordinatorParameters) { + self.parameters = parameters + + let viewModel = LocationSharingViewModel(tileServerMapURL: BuildSettings.tileServerMapURL, + avatarData: parameters.avatarData, + location: parameters.location) + let view = LocationSharingView(context: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + + _locationSharingViewModel = viewModel + locationSharingHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + guard #available(iOS 14.0, *) else { + MXLog.error("[LocationSharingCoordinator] start: Invalid iOS version, returning.") + return + } + + locationSharingViewModel.completion = { [weak self] result in + guard let self = self else { return } + + switch result { + case .cancel: + self.completion?() + case .share(let latitude, let longitude): + if let location = self.parameters.location { + self.showActivityControllerForLocation(location) + return + } + + self.locationSharingViewModel.dispatch(action: .startLoading) + + self.parameters.roomDataSource.sendLocation(withLatitude: latitude, + longitude: longitude, + description: nil) { [weak self] _ in + guard let self = self else { return } + + self.locationSharingViewModel.dispatch(action: .stopLoading(nil)) + self.completion?() + } failure: { [weak self] error in + guard let self = self else { return } + + MXLog.error("[LocationSharingCoordinator] Failed sharing location with error: \(String(describing: error))") + self.locationSharingViewModel.dispatch(action: .stopLoading(error)) + } + } + + } + } + + // MARK: - Presentable + + func toPresentable() -> UIViewController { + return locationSharingHostingController + } + + // MARK: - Private + + private func showActivityControllerForLocation(_ location: CLLocationCoordinate2D) { + let vc = UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)], + applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location), + ShareToMapsAppActivity(type: .google, location: location)]) + + locationSharingHostingController.present(vc, animated: true) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift new file mode 100644 index 000000000..6f7bd3780 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift @@ -0,0 +1,78 @@ +// +// Copyright 2021 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 UIActivity.ActivityType { + static let shareToMapsApp = UIActivity.ActivityType("Element.ShareToMapsApp") +} + +class ShareToMapsAppActivity: UIActivity { + enum MapsAppType { + case apple + case google + } + + let type: MapsAppType + let location: CLLocationCoordinate2D + + private override init() { + fatalError() + } + + init(type: MapsAppType, location: CLLocationCoordinate2D) { + self.type = type + self.location = location + } + + static func urlForMapsAppType(_ type: MapsAppType, location: CLLocationCoordinate2D) -> URL { + switch type { + case .apple: + return URL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)&q=Pin")! + case .google: + return URL(string: "https://www.google.com/maps/search/?api=1&query=\(location.latitude),\(location.longitude)")! + } + } + + override var activityTitle: String? { + switch type { + case .apple: + return VectorL10n.locationSharingOpenAppleMaps + case .google: + return VectorL10n.locationSharingOpenGoogleMaps + } + } + + var activityCategory: UIActivity.Category { + return .action + } + + override var activityType: UIActivity.ActivityType { + return .shareToMapsApp + } + + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { + return true + } + + override func prepare(withActivityItems activityItems: [Any]) { + let url = Self.urlForMapsAppType(type, location: location) + + UIApplication.shared.open(url, options: [:]) { [weak self] result in + self?.activityDidFinish(result) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift new file mode 100644 index 000000000..be9c188f9 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -0,0 +1,85 @@ +// +// Copyright 2021 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 SwiftUI +import Combine +import CoreLocation + +enum LocationSharingViewError { + case failedLoadingMap + case failedLocatingUser + case invalidLocationAuthorization + case failedSharingLocation +} + +enum LocationSharingStateAction { + case error(LocationSharingViewError, LocationSharingViewModelCallback?) + case startLoading + case stopLoading(Error?) +} + +enum LocationSharingViewAction { + case cancel + case share +} + +typealias LocationSharingViewModelCallback = ((LocationSharingViewModelResult) -> Void) + +enum LocationSharingViewModelResult { + case cancel + case share(latitude: Double, longitude: Double) +} + +@available(iOS 14, *) +struct LocationSharingViewState: BindableState { + let tileServerMapURL: URL + let avatarData: AvatarInputProtocol + let location: CLLocationCoordinate2D? + + var showLoadingIndicator: Bool = false + + var shareButtonVisible: Bool { + return location == nil + } + + var shareButtonEnabled: Bool { + !showLoadingIndicator + } + + let errorSubject = PassthroughSubject() + + var bindings = LocationSharingViewStateBindings() +} + +struct LocationSharingViewStateBindings { + var alertInfo: ErrorAlertInfo? + var userLocation: CLLocationCoordinate2D? +} + +struct ErrorAlertInfo: Identifiable { + enum AlertType { + case mapLoadingError + case userLocatingError + case authorizationError + case locationSharingError + } + + let id: AlertType + let title: String + let primaryButton: (title: String, action: (() -> Void)?) + let secondaryButton: (title: String, action: (() -> Void)?)? +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift new file mode 100644 index 000000000..f316d761e --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift @@ -0,0 +1,46 @@ +// +// Copyright 2021 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 SwiftUI +import Keys +import CoreLocation + +@available(iOS 14.0, *) +enum MockLocationSharingScreenState: MockScreenState, CaseIterable { + case shareUserLocation + case displayExistingLocation + + var screenType: Any.Type { + MockLocationSharingScreenState.self + } + + var screenView: ([Any], AnyView) { + + var location: CLLocationCoordinate2D? + if self == .displayExistingLocation { + location = CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) + } + + let mapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! + let viewModel = LocationSharingViewModel(tileServerMapURL: mapURL, + avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Alice"), + location: location) + return ([viewModel], + AnyView(LocationSharingView(context: viewModel.context) + .addDependency(MockAvatarService.example))) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift new file mode 100644 index 000000000..765c0b558 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -0,0 +1,110 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine +import CoreLocation + +@available(iOS 14, *) +typealias LocationSharingViewModelType = StateStoreViewModel< LocationSharingViewState, + LocationSharingStateAction, + LocationSharingViewAction > +@available(iOS 14, *) +class LocationSharingViewModel: LocationSharingViewModelType { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((LocationSharingViewModelResult) -> Void)? + + // MARK: - Setup + + init(tileServerMapURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil) { + let viewState = LocationSharingViewState(tileServerMapURL: tileServerMapURL, avatarData: avatarData, location: location) + super.init(initialViewState: viewState) + + state.errorSubject.sink { [weak self] error in + guard let self = self else { return } + self.dispatch(action: .error(error, self.completion)) + }.store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: LocationSharingViewAction) { + switch viewAction { + case .cancel: + completion?(.cancel) + case .share: + if let location = state.location { + completion?(.share(latitude: location.latitude, longitude: location.longitude)) + return + } + + guard let location = state.bindings.userLocation else { + dispatch(action: .error(.failedLocatingUser, completion)) + return + } + + completion?(.share(latitude: location.latitude, longitude: location.longitude)) + } + } + + override class func reducer(state: inout LocationSharingViewState, action: LocationSharingStateAction) { + switch action { + case .error(let error, let completion): + + switch error { + case .failedLoadingMap: + state.bindings.alertInfo = ErrorAlertInfo(id: .mapLoadingError, + title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) + case .failedLocatingUser: + state.bindings.alertInfo = ErrorAlertInfo(id: .userLocatingError, + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) + case .invalidLocationAuthorization: + state.bindings.alertInfo = ErrorAlertInfo(id: .authorizationError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }), + secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } + })) + default: + break + } + + case .startLoading: + state.showLoadingIndicator = true + case .stopLoading(let error): + state.showLoadingIndicator = false + + if error != nil { + state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, nil), + secondaryButton: nil) + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift new file mode 100644 index 000000000..6e68f3556 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift @@ -0,0 +1,53 @@ +// +// Copyright 2021 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 XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class LocationSharingUITests: XCTestCase { + + private var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + } + + func testInitialUserLocation() { + goToScreenWithIdentifier(MockLocationSharingScreenState.shareUserLocation.title) + + XCTAssertTrue(app.buttons["Cancel"].exists) + XCTAssertTrue(app.buttons["Share"].exists) + XCTAssertTrue(app.otherElements["Map"].exists) + } + + func testInitialExistingLocation() { + goToScreenWithIdentifier(MockLocationSharingScreenState.displayExistingLocation.title) + + XCTAssertTrue(app.buttons["Cancel"].exists) + XCTAssertTrue(app.buttons["location share icon"].exists) + XCTAssertTrue(app.otherElements["Map"].exists) + } + + // Need a delay when showing the map otherwise the simulator breaks + private func goToScreenWithIdentifier(_ identifier: String) { + app.goToScreenWithIdentifier(identifier) + sleep(2) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift new file mode 100644 index 000000000..e3371727c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift @@ -0,0 +1,128 @@ +// +// Copyright 2021 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 XCTest +import Combine +import CoreLocation + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class LocationSharingViewModelTests: XCTestCase { + + var cancellables = Set() + + func testInitialState() { + let viewModel = buildViewModel(withLocation: false) + + XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled) + XCTAssertTrue(viewModel.context.viewState.shareButtonVisible) + XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator) + + XCTAssertNotNil(viewModel.context.viewState.tileServerMapURL) + XCTAssertNotNil(viewModel.context.viewState.avatarData) + + XCTAssertNil(viewModel.context.viewState.location) + XCTAssertNil(viewModel.context.viewState.bindings.userLocation) + XCTAssertNil(viewModel.context.viewState.bindings.alertInfo) + } + + func testCancellation() { + let viewModel = buildViewModel(withLocation: false) + + let expectation = self.expectation(description: "Cancellation completion should be invoked") + + viewModel.completion = { result in + switch result { + case .share: + XCTFail() + case .cancel: + expectation.fulfill() + } + } + + viewModel.context.send(viewAction: .cancel) + + waitForExpectations(timeout: 3) + } + + func testShareNoUserLocation() { + let viewModel = buildViewModel(withLocation: false) + + XCTAssertNil(viewModel.context.viewState.bindings.userLocation) + XCTAssertNil(viewModel.context.viewState.location) + + viewModel.context.send(viewAction: .share) + + XCTAssertNotNil(viewModel.context.viewState.bindings.alertInfo) + XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, .userLocatingError) + } + + func testShareExistingLocation() { + let viewModel = buildViewModel(withLocation: true) + + let expectation = self.expectation(description: "Share completion should be invoked") + + viewModel.completion = { result in + switch result { + case .share(let latitude, let longitude): + XCTAssertEqual(latitude, viewModel.context.viewState.location?.latitude) + XCTAssertEqual(longitude, viewModel.context.viewState.location?.longitude) + expectation.fulfill() + case .cancel: + XCTFail() + } + } + + XCTAssertNil(viewModel.context.viewState.bindings.userLocation) + XCTAssertNotNil(viewModel.context.viewState.location) + + viewModel.context.send(viewAction: .share) + + XCTAssertNil(viewModel.context.viewState.bindings.alertInfo) + + waitForExpectations(timeout: 3) + } + + func testLoading() { + let viewModel = buildViewModel(withLocation: false) + + viewModel.dispatch(action: .startLoading) + + XCTAssertFalse(viewModel.context.viewState.shareButtonEnabled) + XCTAssertTrue(viewModel.context.viewState.showLoadingIndicator) + + viewModel.dispatch(action: .stopLoading(nil)) + + XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled) + XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator) + } + + func testInvalidLocationAuthorization() { + let viewModel = buildViewModel(withLocation: false) + + viewModel.context.viewState.errorSubject.send(.invalidLocationAuthorization) + + XCTAssertNotNil(viewModel.context.alertInfo) + XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, .authorizationError) + } + + private func buildViewModel(withLocation: Bool) -> LocationSharingViewModel { + LocationSharingViewModel(tileServerMapURL: URL(string: "http://empty.com")!, + avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""), + location: (withLocation ? CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) : nil)) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift new file mode 100644 index 000000000..53064ae54 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -0,0 +1,132 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Combine +import Mapbox + +@available(iOS 14, *) +struct LocationSharingMapView: UIViewRepresentable { + private struct Constants { + static let mapZoomLevel = 15.0 + } + + let tileServerMapURL: URL + let avatarData: AvatarInputProtocol + let location: CLLocationCoordinate2D? + + let errorSubject: PassthroughSubject + @Binding var userLocation: CLLocationCoordinate2D? + + func makeUIView(context: Context) -> some UIView { + let mapView = MGLMapView(frame: .zero, styleURL: tileServerMapURL) + mapView.delegate = context.coordinator + + mapView.logoView.isHidden = true + mapView.attributionButton.isHidden = true + + if let location = location { + mapView.setCenter(location, zoomLevel: Constants.mapZoomLevel, animated: false) + + let pointAnnotation = MGLPointAnnotation() + pointAnnotation.coordinate = location + mapView.addAnnotation(pointAnnotation) + } else { + mapView.showsUserLocation = true + mapView.userTrackingMode = .follow + } + + return mapView + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + + } + + func makeCoordinator() -> LocationSharingMapViewCoordinator { + LocationSharingMapViewCoordinator(avatarData: avatarData, + errorSubject: errorSubject, + userLocation: $userLocation) + } +} + +@available(iOS 14, *) +class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate { + + private let avatarData: AvatarInputProtocol + private let errorSubject: PassthroughSubject + @Binding var userLocation: CLLocationCoordinate2D? + + init(avatarData: AvatarInputProtocol, + errorSubject: PassthroughSubject, + userLocation: Binding) { + self.avatarData = avatarData + self.errorSubject = errorSubject + self._userLocation = userLocation + } + + // MARK: - MGLMapViewDelegate + + func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { + return UserLocationAnnotatonView(avatarData: avatarData) + } + + func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) { + errorSubject.send(.failedLoadingMap) + } + + func mapView(_ mapView: MGLMapView, didFailToLocateUserWithError error: Error) { + errorSubject.send(.failedLocatingUser) + } + + func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { + self.userLocation = userLocation?.coordinate + } + + func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) { + switch manager.authorizationStatus { + case .restricted: + fallthrough + case .denied: + errorSubject.send(.failedLocatingUser) + default: + break + } + } +} + +@available(iOS 14, *) +private class UserLocationAnnotatonView: MGLUserLocationAnnotationView { + + init(avatarData: AvatarInputProtocol) { + super.init(frame: .zero) + + guard let avatarImageView = UIHostingController(rootView: LocationSharingUserMarkerView(avatarData: avatarData)).view else { + return + } + + addSubview(avatarImageView) + + addConstraints([topAnchor.constraint(equalTo: avatarImageView.topAnchor), + leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor), + bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), + trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor)]) + } + + required init?(coder: NSCoder) { + fatalError() + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift new file mode 100644 index 000000000..0727e0b5b --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift @@ -0,0 +1,53 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct LocationSharingUserMarkerView: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + let avatarData: AvatarInputProtocol + + var body: some View { + ZStack(alignment: .center) { + Image(uiImage: Asset.Images.locationUserMarker.image) + AvatarImage(avatarData: avatarData, size: .large) + .offset(.init(width: 0.0, height: -1.5)) + } + .accentColor(theme.colors.accent) + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct LocationSharingUserMarkerView_Previews: PreviewProvider { + static var previews: some View { + let avatarData = AvatarInput(mxContentUri: "", + matrixItemId: "", + displayName: "Alice") + + LocationSharingUserMarkerView(avatarData: avatarData) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift new file mode 100644 index 000000000..9bc932536 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -0,0 +1,106 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import CoreLocation + +@available(iOS 14.0, *) +struct LocationSharingView: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var context: LocationSharingViewModel.Context + + var body: some View { + NavigationView { + LocationSharingMapView(tileServerMapURL: context.viewState.tileServerMapURL, + avatarData: context.viewState.avatarData, + location: context.viewState.location, + errorSubject: context.viewState.errorSubject, + userLocation: $context.userLocation) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(VectorL10n.cancel, action: { + context.send(viewAction: .cancel) + }) + } + ToolbarItem(placement: .principal) { + Text(VectorL10n.locationSharingTitle) + .font(.headline) + .foregroundColor(theme.colors.primaryContent) + } + ToolbarItem(placement: .navigationBarTrailing) { + if context.viewState.location != nil { + Button { + context.send(viewAction: .share) + } label: { + Image(uiImage: Asset.Images.locationShareIcon.image) + } + .disabled(!context.viewState.shareButtonEnabled) + } else { + Button(VectorL10n.locationSharingShareAction, action: { + context.send(viewAction: .share) + }) + .disabled(!context.viewState.shareButtonEnabled) + } + } + } + .navigationBarTitleDisplayMode(.inline) + .ignoresSafeArea() + .alert(item: $context.alertInfo) { info in + if let secondaryButton = info.secondaryButton { + return Alert(title: Text(info.title), + primaryButton: .default(Text(info.primaryButton.title)) { + info.primaryButton.action?() + }, + secondaryButton: .default(Text(secondaryButton.title)) { + secondaryButton.action?() + }) + } else { + return Alert(title: Text(info.title), + dismissButton: .default(Text(info.primaryButton.title)) { + info.primaryButton.action?() + }) + } + } + } + .accentColor(theme.colors.accent) + .activityIndicator(show: context.viewState.showLoadingIndicator) + } + + @ViewBuilder + private var activityIndicator: some View { + if context.viewState.showLoadingIndicator { + ActivityIndicator() + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct LocationSharingView_Previews: PreviewProvider { + static let stateRenderer = MockLocationSharingScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index da63d7098..ee0f60a78 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -21,11 +21,10 @@ import UIKit import SwiftUI struct PollEditFormCoordinatorParameters { - let navigationRouter: NavigationRouterType? let room: MXRoom } -final class PollEditFormCoordinator: Coordinator { +final class PollEditFormCoordinator: Coordinator, Presentable { // MARK: - Properties @@ -42,9 +41,10 @@ final class PollEditFormCoordinator: Coordinator { // MARK: Public - // Must be used only internally var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + // MARK: - Setup @available(iOS 14.0, *) @@ -65,13 +65,11 @@ final class PollEditFormCoordinator: Coordinator { return } - parameters.navigationRouter?.present(pollEditFormHostingController, animated: true) - pollEditFormViewModel.completion = { [weak self] result in guard let self = self else { return } switch result { case .cancel: - self.parameters.navigationRouter?.dismissModule(animated: true, completion: nil) + self.completion?() case .create(let question, let answerOptions): var options = [MXEventContentPollStartAnswerOption]() for answerOption in answerOptions { @@ -88,8 +86,8 @@ final class PollEditFormCoordinator: Coordinator { self.parameters.room.sendPollStart(withContent: pollStartContent, localEcho: nil) { [weak self] result in guard let self = self else { return } - self.parameters.navigationRouter?.dismissModule(animated: true, completion: nil) self.pollEditFormViewModel.dispatch(action: .stopLoading(nil)) + self.completion?() } failure: { [weak self] error in guard let self = self else { return } @@ -99,4 +97,10 @@ final class PollEditFormCoordinator: Coordinator { } } } + + // MARK: - Private + + func toPresentable() -> UIViewController { + return pollEditFormHostingController + } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index 9e81488a5..57f3a63a3 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -87,7 +87,7 @@ struct PollEditForm: View { .alert(isPresented: $viewModel.showsFailureAlert) { Alert(title: Text(VectorL10n.pollEditFormPostFailureTitle), message: Text(VectorL10n.pollEditFormPostFailureSubtitle), - dismissButton: .default(Text(VectorL10n.pollEditFormPostFailureAction))) + dismissButton: .default(Text(VectorL10n.ok))) } .frame(minHeight: proxy.size.height) // Make the VStack fill the ScrollView's parent .toolbar { diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift index 8d201264f..78c53acec 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift @@ -27,7 +27,7 @@ struct PollTimelineCoordinatorParameters { } @available(iOS 14.0, *) -final class PollTimelineCoordinator: Coordinator, PollAggregatorDelegate { +final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDelegate { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift b/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift index 22a94bb68..70efcc067 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift +++ b/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift @@ -50,7 +50,7 @@ struct PollTimelineView: View { .alert(isPresented: $viewModel.showsClosingFailureAlert) { Alert(title: Text(VectorL10n.pollTimelineNotClosedTitle), message: Text(VectorL10n.pollTimelineNotClosedSubtitle), - dismissButton: .default(Text(VectorL10n.pollTimelineNotClosedAction))) + dismissButton: .default(Text(VectorL10n.ok))) } } .disabled(poll.closed) @@ -62,7 +62,7 @@ struct PollTimelineView: View { .alert(isPresented: $viewModel.showsAnsweringFailureAlert) { Alert(title: Text(VectorL10n.pollTimelineVoteNotRegisteredTitle), message: Text(VectorL10n.pollTimelineVoteNotRegisteredSubtitle), - dismissButton: .default(Text(VectorL10n.pollTimelineVoteNotRegisteredAction))) + dismissButton: .default(Text(VectorL10n.ok))) } } .padding([.horizontal, .top], 2.0) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index e7308710d..d960a9796 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -26,7 +26,7 @@ protocol UserSuggestionCoordinatorDelegate: AnyObject { } @available(iOS 14.0, *) -final class UserSuggestionCoordinator: Coordinator { +final class UserSuggestionCoordinator: Coordinator, Presentable { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index 3eb01bbdd..da0b965be 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -20,7 +20,7 @@ struct TemplateUserProfileCoordinatorParameters { let session: MXSession } -final class TemplateUserProfileCoordinator: Coordinator { +final class TemplateUserProfileCoordinator: Coordinator, Presentable { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift index c7a79d69f..d7af25881 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift @@ -19,7 +19,7 @@ import UIKit @objcMembers -final class TemplateRoomsCoordinator: Coordinator { +final class TemplateRoomsCoordinator: Coordinator, Presentable { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift index a749cde50..821f4ccac 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift @@ -96,14 +96,14 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { return events .filter({ event in event.type == kMXEventTypeStringRoomMessage - && event.content["msgtype"] as? String == kMXMessageTypeText + && event.content[kMXMessageTypeKey] as? String == kMXMessageTypeText // TODO: New to our SwiftUI Template? Why not implement another message type like image? }) .compactMap({ event -> TemplateRoomChatMessage? in guard let eventId = event.eventId, - let body = event.content["body"] as? String, + let body = event.content[kMXMessageBodyKey] as? String, let sender = senderForMessage(event: event) else { return nil } diff --git a/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h b/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h new file mode 100644 index 000000000..b75953b6e --- /dev/null +++ b/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "BuildInfo.h" diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index ecbf8a91c..66d318a6a 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -31,6 +31,7 @@ targets: platform: iOS dependencies: - target: DesignKit + - package: Mapbox sources: - path: . excludes: @@ -38,6 +39,8 @@ targets: - "**/MatrixSDK/**" - "**/Coordinator/**" - "**/Test/**" + - path: ../Riot/Managers/AppInfo/ + - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Generated/Strings.swift - path: ../Riot/Generated/Images.swift - path: ../Riot/Managers/Theme/ThemeIdentifier.swift diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index 0e2a100df..68de22ac5 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -37,6 +37,8 @@ targets: base: TEST_TARGET_NAME: RiotSwiftUI PRODUCT_BUNDLE_IDENTIFIER: org.matrix.RiotSwiftUITests$(rfc1034identifier) + SWIFT_OBJC_BRIDGING_HEADER: $(SRCROOT)/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h + SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h sources: # Source included/excluded here here are similar to RiotSwiftUI as we # need access to ScreenStates @@ -45,6 +47,8 @@ targets: - "**/MatrixSDK/**" - "**/Coordinator/**" - "**/Test/Unit/**" + - path: ../Riot/Managers/AppInfo/ + - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Generated/Strings.swift - path: ../Riot/Generated/Images.swift - path: ../Riot/Managers/Theme/ThemeIdentifier.swift diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m index fb3e28af7..440022c84 100644 --- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m +++ b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m @@ -51,10 +51,8 @@ anEvent.eventId = @"anEventId"; anEvent.wireType = kMXEventTypeStringRoomMessage; anEvent.originServerTs = (uint64_t) ([[NSDate date] timeIntervalSince1970] * 1000); - anEvent.wireContent = @{ - @"msgtype": kMXMessageTypeText, - @"body": @"deded", - }; + anEvent.wireContent = @{ kMXMessageTypeKey: kMXMessageTypeText, + kMXMessageBodyKey: @"deded" }; maxHeaderSize = ceil(eventFormatter.defaultTextFont.pointSize * 1.2); } diff --git a/project.yml b/project.yml index 44c6eeb12..dcd15f63b 100644 --- a/project.yml +++ b/project.yml @@ -35,3 +35,9 @@ include: - path: RiotSwiftUI/target.yml - path: RiotSwiftUI/targetUnitTests.yml - path: RiotSwiftUI/targetUITests.yml + +packages: + Mapbox: + url: https://github.com/maplibre/maplibre-gl-native-distribution + minVersion: 5.12.2 + maxVersion: 5.13.0 \ No newline at end of file