Merge branch 'gil/SP1_space_creation' into gil/143_create_public_space

# Conflicts:
#	Riot/Generated/Strings.swift
#	Riot/Assets/en.lproj/Vector.strings
This commit is contained in:
Gil Eluard 2022-01-10 09:50:00 +01:00
commit bce0bf3d4a
99 changed files with 1925 additions and 209 deletions

View file

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

View file

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

View file

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

View file

@ -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
}
}

View file

@ -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)

View file

@ -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

View file

@ -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|

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,005 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -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.";

View file

@ -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 %@";

View file

@ -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, well 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, well 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, well 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 hasnt been implemented here, but its on the way. For now, you can do that with Element on your computer.";
"spaces_coming_soon_detail" = "This feature hasnt been implemented here, but its 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";

View file

@ -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")

View file

@ -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")

View file

@ -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, well 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, well 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, well 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 hasnt been implemented here, but its 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 hasnt been implemented here, but its 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 {

View file

@ -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
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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"];

View file

@ -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

View file

@ -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.

View file

@ -33,7 +33,6 @@ typedef enum : NSUInteger {
MXKAttachmentTypeAudio,
MXKAttachmentTypeVoiceMessage,
MXKAttachmentTypeVideo,
MXKAttachmentTypeLocation,
MXKAttachmentTypeFile,
MXKAttachmentTypeSticker

View file

@ -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"]);

View file

@ -102,7 +102,7 @@ static NSAttributedString *messageSeparator = nil;
return NO;
}
}
// Add all components of the provided message
for (MXKRoomBubbleComponent* component in cellData.bubbleComponents)
{

View file

@ -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]))
{

View file

@ -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.

View file

@ -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]))

View file

@ -45,6 +45,11 @@
return [MatrixKitL10n messageReplyToSenderSentAFile];
}
- (NSString *)senderSentTheirLocation
{
return [MatrixKitL10n messageReplyToSenderSentTheirLocation];
}
- (NSString *)messageToReplyToPrefix
{
return [MatrixKitL10n messageReplyToMessageToReplyToPrefix];

View file

@ -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;
}

View file

@ -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])
{

View file

@ -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) {

View file

@ -32,7 +32,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag)
RoomBubbleCellDataTagCall,
RoomBubbleCellDataTagGroupCall,
RoomBubbleCellDataTagRoomCreationIntro,
RoomBubbleCellDataTagPoll
RoomBubbleCellDataTagPoll,
RoomBubbleCellDataTagLocation
};
/**

View file

@ -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;

View file

@ -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])
{

View 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)
}
}

View 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>

View 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
}
}

View 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>

View file

@ -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 {

View file

@ -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;

View file

@ -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])
{

View file

@ -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"/>

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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()))))

View file

@ -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;

View file

@ -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)
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -35,6 +35,7 @@ targets:
- target: SiriIntents
- target: RiotNSE
- target: DesignKit
- package: Mapbox
configFiles:
Debug: Debug.xcconfig

View file

@ -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])

View file

@ -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

View file

@ -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>

View file

@ -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
}

View file

@ -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?()
}
}

View file

@ -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)

View file

@ -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()
}
}
}

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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)?)?
}

View file

@ -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)))
}
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}

View file

@ -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))
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -27,7 +27,7 @@ struct PollTimelineCoordinatorParameters {
}
@available(iOS 14.0, *)
final class PollTimelineCoordinator: Coordinator, PollAggregatorDelegate {
final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDelegate {
// MARK: - Properties

View file

@ -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)

View file

@ -26,7 +26,7 @@ protocol UserSuggestionCoordinatorDelegate: AnyObject {
}
@available(iOS 14.0, *)
final class UserSuggestionCoordinator: Coordinator {
final class UserSuggestionCoordinator: Coordinator, Presentable {
// MARK: - Properties

View file

@ -20,7 +20,7 @@ struct TemplateUserProfileCoordinatorParameters {
let session: MXSession
}
final class TemplateUserProfileCoordinator: Coordinator {
final class TemplateUserProfileCoordinator: Coordinator, Presentable {
// MARK: - Properties

View file

@ -19,7 +19,7 @@
import UIKit
@objcMembers
final class TemplateRoomsCoordinator: Coordinator {
final class TemplateRoomsCoordinator: Coordinator, Presentable {
// MARK: - Properties

View file

@ -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 }

View 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"

View file

@ -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

View file

@ -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

View file

@ -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);
}

View file

@ -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