Merge branch 'gil/SP1_space_creation' into gil/143_create_public_space
# Conflicts: # Riot/Generated/Strings.swift # Riot/Assets/en.lproj/Vector.strings
1
.github/workflows/ci-build.yml
vendored
|
@ -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:
|
||||
|
|
1
.github/workflows/ci-tests.yml
vendored
|
@ -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:
|
||||
|
|
1
.github/workflows/release-alpha.yml
vendored
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
1
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)
|
||||
|
|
69
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
|
||||
|
|
6
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|
|
||||
|
|
23
Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png
vendored
Normal file
After Width: | Height: | Size: 529 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@2x.png
vendored
Normal file
After Width: | Height: | Size: 907 B |
BIN
Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
6
Riot/Assets/Images.xcassets/Room/Location/Contents.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
23
Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png
vendored
Normal file
After Width: | Height: | Size: 529 B |
BIN
Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 907 B |
BIN
Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
23
Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png
vendored
Normal file
After Width: | Height: | Size: 384 B |
BIN
Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@2x.png
vendored
Normal file
After Width: | Height: | Size: 596 B |
BIN
Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@3x.png
vendored
Normal file
After Width: | Height: | Size: 819 B |
23
Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png
vendored
Normal file
After Width: | Height: | Size: 1,005 B |
BIN
Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png
vendored
Normal file
After Width: | Height: | Size: 2.6 KiB |
|
@ -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.";
|
||||
|
|
|
@ -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 %@";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -33,7 +33,6 @@ typedef enum : NSUInteger {
|
|||
MXKAttachmentTypeAudio,
|
||||
MXKAttachmentTypeVoiceMessage,
|
||||
MXKAttachmentTypeVideo,
|
||||
MXKAttachmentTypeLocation,
|
||||
MXKAttachmentTypeFile,
|
||||
MXKAttachmentTypeSticker
|
||||
|
||||
|
|
|
@ -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"]);
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ static NSAttributedString *messageSeparator = nil;
|
|||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add all components of the provided message
|
||||
for (MXKRoomBubbleComponent* component in cellData.bubbleComponents)
|
||||
{
|
||||
|
|
|
@ -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]))
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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<NSString*, id>*)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]))
|
||||
|
|
|
@ -45,6 +45,11 @@
|
|||
return [MatrixKitL10n messageReplyToSenderSentAFile];
|
||||
}
|
||||
|
||||
- (NSString *)senderSentTheirLocation
|
||||
{
|
||||
return [MatrixKitL10n messageReplyToSenderSentTheirLocation];
|
||||
}
|
||||
|
||||
- (NSString *)messageToReplyToPrefix
|
||||
{
|
||||
return [MatrixKitL10n messageReplyToMessageToReplyToPrefix];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -175,10 +175,6 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
|||
{
|
||||
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 = @"<a href=\"(.*?)\">([^<]*)</a>";
|
|||
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 = @"<a href=\"(.*?)\">([^<]*)</a>";
|
|||
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:@"<mx-reply><blockquote><a href=\"%@\">In reply to</a> <a href=\"%@\">%@</a><br>%@</blockquote></mx-reply>%@",
|
||||
[MXTools permalinkToEvent:eventThreadIdentifier inRoom:event.roomId],
|
||||
[MXTools permalinkToUserWithUserId:threadRootEvent.sender],
|
||||
|
@ -1283,7 +1279,7 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
|||
}
|
||||
else
|
||||
{
|
||||
MXJSONModelSetString(body, event.content[@"body"]);
|
||||
MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
|
||||
}
|
||||
|
||||
if (body)
|
||||
|
@ -1333,23 +1329,6 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
|||
*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 = @"<a href=\"(.*?)\">([^<]*)</a>";
|
|||
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 = @"<a href=\"(.*?)\">([^<]*)</a>";
|
|||
|
||||
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 = @"<a href=\"(.*?)\">([^<]*)</a>";
|
|||
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])
|
||||
{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -32,7 +32,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag)
|
|||
RoomBubbleCellDataTagCall,
|
||||
RoomBubbleCellDataTagGroupCall,
|
||||
RoomBubbleCellDataTagRoomCreationIntro,
|
||||
RoomBubbleCellDataTagPoll
|
||||
RoomBubbleCellDataTagPoll,
|
||||
RoomBubbleCellDataTagLocation
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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])
|
||||
{
|
||||
|
|
33
Riot/Modules/Room/Location/LocationUserMarkerView.swift
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
46
Riot/Modules/Room/Location/LocationUserMarkerView.xib
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="LocationUserMarkerView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="54"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="location_user_marker" translatesAutoresizingMaskIntoConstraints="NO" id="ldO-kc-R5W">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="54"/>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qut-wn-BX3" customClass="UserAvatarView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="2" y="2" width="46" height="46"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="ldO-kc-R5W" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="VF5-CP-8eH"/>
|
||||
<constraint firstItem="ldO-kc-R5W" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="Voc-LH-fTw"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ldO-kc-R5W" secondAttribute="trailing" id="Vt0-UN-s20"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ldO-kc-R5W" secondAttribute="bottom" id="Ybf-8x-UaG"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="avatarView" destination="qut-wn-BX3" id="wHA-bz-A2y"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-84.057971014492765" y="-80.357142857142847"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="location_user_marker" width="51" height="54.5"/>
|
||||
</resources>
|
||||
</document>
|
114
Riot/Modules/Room/Location/RoomTimelineLocationView.swift
Normal file
|
@ -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
|
||||
}
|
||||
}
|
72
Riot/Modules/Room/Location/RoomTimelineLocationView.xib
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="RoomTimelineLocationView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="395" height="250"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oVd-gS-Rmb">
|
||||
<rect key="frame" x="0.0" y="210" width="395" height="40"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="XHz-4S-fh4">
|
||||
<rect key="frame" x="12" y="8" width="371" height="24"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="location_marker_icon" translatesAutoresizingMaskIntoConstraints="NO" id="GP2-dA-giJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="24" height="24"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="24" id="7nK-Kb-7Iq"/>
|
||||
<constraint firstAttribute="height" constant="24" id="nBW-gN-0uW"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c68-l7-McA">
|
||||
<rect key="frame" x="32" y="2" width="339" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="XHz-4S-fh4" secondAttribute="trailing" constant="12" id="FI1-B7-bPV"/>
|
||||
<constraint firstItem="XHz-4S-fh4" firstAttribute="top" secondItem="oVd-gS-Rmb" secondAttribute="top" constant="8" id="UJq-Yz-ikR"/>
|
||||
<constraint firstAttribute="bottom" secondItem="XHz-4S-fh4" secondAttribute="bottom" constant="8" id="cvr-Gb-uLe"/>
|
||||
<constraint firstItem="XHz-4S-fh4" firstAttribute="leading" secondItem="oVd-gS-Rmb" secondAttribute="leading" constant="12" id="wSE-NS-2h4"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="oVd-gS-Rmb" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="6Vw-QI-iN2"/>
|
||||
<constraint firstItem="oVd-gS-Rmb" firstAttribute="bottom" secondItem="vUN-kp-3ea" secondAttribute="bottom" id="FVf-yb-Gxc"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="oVd-gS-Rmb" secondAttribute="trailing" id="O3u-fm-TxC"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="descriptionContainerView" destination="oVd-gS-Rmb" id="Npu-jp-oYo"/>
|
||||
<outlet property="descriptionLabel" destination="c68-l7-McA" id="HiH-8Q-yTp"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="165.94202898550725" y="-100.78125"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="location_marker_icon" width="24" height="24"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -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 {
|
||||
|
|
|
@ -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<MXKRoomBubbleCellDataStoring>)bubbleData;
|
||||
|
||||
- (BOOL)roomViewController:(RoomViewController *)roomViewController
|
||||
canEndPollWithEventIdentifier:(NSString *)eventIdentifier;
|
||||
|
||||
|
|
|
@ -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])
|
||||
{
|
||||
|
|
|
@ -140,14 +140,14 @@
|
|||
</constraints>
|
||||
</stackView>
|
||||
<view clipsSubviews="YES" contentMode="scaleAspectFit" translatesAutoresizingMaskIntoConstraints="NO" id="oeI-eO-mFK">
|
||||
<rect key="frame" x="56" y="3" width="524" height="91"/>
|
||||
<rect key="frame" x="56" y="3" width="524" height="78"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="oeI-eO-mFK" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="vcq-cR-uBc" secondAttribute="leading" constant="56" id="0Fr-0L-9tU"/>
|
||||
<constraint firstAttribute="bottom" secondItem="oeI-eO-mFK" secondAttribute="bottom" constant="3" id="8M5-uW-82s"/>
|
||||
<constraint firstAttribute="bottom" secondItem="oeI-eO-mFK" secondAttribute="bottom" constant="16" id="8M5-uW-82s"/>
|
||||
<constraint firstItem="oeI-eO-mFK" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="4d4-XQ-ido" secondAttribute="trailing" constant="6" id="9By-U1-wTY"/>
|
||||
<constraint firstAttribute="trailing" secondItem="oeI-eO-mFK" secondAttribute="trailing" constant="15" id="Pbe-4d-q6Y"/>
|
||||
<constraint firstAttribute="bottom" secondItem="4d4-XQ-ido" secondAttribute="bottom" id="Tkw-p1-CYF"/>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ struct VoiceMessageAudioConverter {
|
|||
|
||||
static func mediaDurationAt(_ sourceURL: URL, completion: @escaping (Result<TimeInterval, VoiceMessageAudioConverterError>) -> 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()))))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -65,6 +65,8 @@
|
|||
<string>The photo library is used to send photos and videos.</string>
|
||||
<key>NSSiriUsageDescription</key>
|
||||
<string>Siri is used to perform calls even from the lock screen.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>When you share your location to people, Element needs access to show them a map.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
|
|
|
@ -35,6 +35,7 @@ targets:
|
|||
- target: SiriIntents
|
||||
- target: RiotNSE
|
||||
- target: DesignKit
|
||||
- package: Mapbox
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -20,5 +20,9 @@
|
|||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>RiotSwiftUI</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>When you share your location to people, Element needs access to show them a map.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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?()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<LocationSharingViewError, Never>()
|
||||
|
||||
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)?)?
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -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<LocationSharingViewError, Never>
|
||||
@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<LocationSharingViewError, Never>
|
||||
@Binding var userLocation: CLLocationCoordinate2D?
|
||||
|
||||
init(avatarData: AvatarInputProtocol,
|
||||
errorSubject: PassthroughSubject<LocationSharingViewError, Never>,
|
||||
userLocation: Binding<CLLocationCoordinate2D?>) {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -27,7 +27,7 @@ struct PollTimelineCoordinatorParameters {
|
|||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
final class PollTimelineCoordinator: Coordinator, PollAggregatorDelegate {
|
||||
final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDelegate {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -26,7 +26,7 @@ protocol UserSuggestionCoordinatorDelegate: AnyObject {
|
|||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
final class UserSuggestionCoordinator: Coordinator {
|
||||
final class UserSuggestionCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ struct TemplateUserProfileCoordinatorParameters {
|
|||
let session: MXSession
|
||||
}
|
||||
|
||||
final class TemplateUserProfileCoordinator: Coordinator {
|
||||
final class TemplateUserProfileCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import UIKit
|
||||
|
||||
@objcMembers
|
||||
final class TemplateRoomsCoordinator: Coordinator {
|
||||
final class TemplateRoomsCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
5
RiotSwiftUI/RiotSwiftUI-Bridging-Header.h
Normal file
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|