Open map from timeline (#1199)

* Add navigation to expaneded map

* Add MapLibreMapView.Options

* Add AppActivityView

* Add ShareToMapsAppActivity

* Add share sheet presentation

* Add localisations

* Cleanup

* Fix UT build errors

* Revert breaking change

* Fix UIView setup

* Add support for location’s description

* Show popover on iPad

* Restore assets

* More cleanup
This commit is contained in:
Alfonso Grillo
2023-06-29 11:12:42 +02:00
committed by GitHub
parent 2cd03a9e7e
commit eb65619979
22 changed files with 489 additions and 150 deletions

View File

@@ -75,6 +75,7 @@
1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; }; 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */; };
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; }; 1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; };
1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */; }; 1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */; };
1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */; };
1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */; }; 1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */; };
1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; }; 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; };
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; };
@@ -370,6 +371,7 @@
8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */; }; 8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */; };
8B76191B9DDD1AC90A6E3A35 /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */; }; 8B76191B9DDD1AC90A6E3A35 /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */; };
8BC8EF6705A78946C1F22891 /* SoftLogoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71A7D4DDEEE5D2CA0C8D63CD /* SoftLogoutScreen.swift */; }; 8BC8EF6705A78946C1F22891 /* SoftLogoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71A7D4DDEEE5D2CA0C8D63CD /* SoftLogoutScreen.swift */; };
8C1A5ECAF895D4CAF8C4D461 /* AppActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F21ED7205048668BEB44A38 /* AppActivityView.swift */; };
8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; }; 8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; };
8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; };
8D0C5BC670D514760CC84E2A /* TextBasedRoomTimelineViewMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A542BC40D6EC2E66BC5659B /* TextBasedRoomTimelineViewMock.swift */; }; 8D0C5BC670D514760CC84E2A /* TextBasedRoomTimelineViewMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A542BC40D6EC2E66BC5659B /* TextBasedRoomTimelineViewMock.swift */; };
@@ -931,6 +933,7 @@
42ADEA322D2089391E049535 /* InvitesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreen.swift; sourceTree = "<group>"; }; 42ADEA322D2089391E049535 /* InvitesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreen.swift; sourceTree = "<group>"; };
42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = "<group>"; }; 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = "<group>"; };
42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineView.swift; sourceTree = "<group>"; }; 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineView.swift; sourceTree = "<group>"; };
4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToMapsAppActivity.swift; sourceTree = "<group>"; };
44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationTests.swift; sourceTree = "<group>"; }; 44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationTests.swift; sourceTree = "<group>"; };
450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = "<group>"; }; 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = "<group>"; };
4549FCB53F43DB0B278374BC /* TemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreen.swift; sourceTree = "<group>"; }; 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreen.swift; sourceTree = "<group>"; };
@@ -1109,6 +1112,7 @@
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; }; 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = "<group>"; }; 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = "<group>"; };
8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = "<group>"; }; 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = "<group>"; };
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; };
8F61A0DD8243B395499C99A2 /* InvitesScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenUITests.swift; sourceTree = "<group>"; }; 8F61A0DD8243B395499C99A2 /* InvitesScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenUITests.swift; sourceTree = "<group>"; };
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; }; 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; };
8FC26871038FB0E4AAE22605 /* apple_emojis_data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = apple_emojis_data.json; sourceTree = "<group>"; }; 8FC26871038FB0E4AAE22605 /* apple_emojis_data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = apple_emojis_data.json; sourceTree = "<group>"; };
@@ -1785,6 +1789,7 @@
328DD5DA1281F758B72006C7 /* Views */ = { 328DD5DA1281F758B72006C7 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
8F21ED7205048668BEB44A38 /* AppActivityView.swift */,
CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */, CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */,
0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */, 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */,
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */, B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */,
@@ -3036,6 +3041,7 @@
F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */, F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */,
53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */, 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */,
DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */, DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */,
4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */,
BB3073CCD77D906B330BC1D6 /* Tests.swift */, BB3073CCD77D906B330BC1D6 /* Tests.swift */,
1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */, 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */,
35FA991289149D31F4286747 /* UserPreference.swift */, 35FA991289149D31F4286747 /* UserPreference.swift */,
@@ -3978,6 +3984,7 @@
7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */, 7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */,
A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */, A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */,
654E802C127B84554042903E /* AnalyticsSettingsScreenViewModelProtocol.swift in Sources */, 654E802C127B84554042903E /* AnalyticsSettingsScreenViewModelProtocol.swift in Sources */,
8C1A5ECAF895D4CAF8C4D461 /* AppActivityView.swift in Sources */,
095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */, 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */,
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */, A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */,
4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */, 4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */,
@@ -4317,6 +4324,7 @@
B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */, B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */,
E0B6A569AC3E81D233B43D60 /* SettingsScreenViewModel.swift in Sources */, E0B6A569AC3E81D233B43D60 /* SettingsScreenViewModel.swift in Sources */,
A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */, A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */,
1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */,
8922219C5C934C4155E8CA50 /* SharedUserDefaultsKeys.swift in Sources */, 8922219C5C934C4155E8CA50 /* SharedUserDefaultsKeys.swift in Sources */,
274CE3C986841D15FD530BF5 /* ShimmerModifier.swift in Sources */, 274CE3C986841D15FD530BF5 /* ShimmerModifier.swift in Sources */,
8BC8EF6705A78946C1F22891 /* SoftLogoutScreen.swift in Sources */, 8BC8EF6705A78946C1F22891 /* SoftLogoutScreen.swift in Sources */,

View File

@@ -319,11 +319,15 @@
"screen_session_verification_waiting_to_accept_title" = "Waiting to accept request"; "screen_session_verification_waiting_to_accept_title" = "Waiting to accept request";
"screen_share_location_title" = "Share location"; "screen_share_location_title" = "Share location";
"screen_share_my_location_action" = "Share my location"; "screen_share_my_location_action" = "Share my location";
"screen_share_open_apple_maps" = "Open in Apple Maps";
"screen_share_open_google_maps" = "Open in Google Maps";
"screen_share_open_osm_maps" = "Open in OpenStreetMap";
"screen_share_this_location_action" = "Share this location"; "screen_share_this_location_action" = "Share this location";
"screen_signout_confirmation_dialog_content" = "Are you sure you want to sign out?"; "screen_signout_confirmation_dialog_content" = "Are you sure you want to sign out?";
"screen_signout_confirmation_dialog_title" = "Sign out"; "screen_signout_confirmation_dialog_title" = "Sign out";
"screen_signout_in_progress_dialog_content" = "Signing out…"; "screen_signout_in_progress_dialog_content" = "Signing out…";
"screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat"; "screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat";
"screen_view_location_title" = "Location";
"screen_waitlist_message" = "There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again.\n\nThanks for your patience!"; "screen_waitlist_message" = "There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again.\n\nThanks for your patience!";
"screen_waitlist_message_success" = "Welcome to %1$@"; "screen_waitlist_message_success" = "Welcome to %1$@";
"screen_waitlist_title" = "You're on the waitlist!"; "screen_waitlist_title" = "You're on the waitlist!";

View File

@@ -145,9 +145,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return .messageForwarding(roomID: roomID, itemID: itemID) return .messageForwarding(roomID: roomID, itemID: itemID)
case (.dismissMessageForwarding, .messageForwarding(let roomID, _)): case (.dismissMessageForwarding, .messageForwarding(let roomID, _)):
return .room(roomID: roomID) return .room(roomID: roomID)
case (.presentLocationPicker, .room(let roomID)): case (.presentMapNavigator, .room(let roomID)):
return .locationPicker(roomID: roomID) return .mapNavigator(roomID: roomID)
case (.dismissLocationPicker, .locationPicker(let roomID)): case (.dismissMapNavigator, .mapNavigator(let roomID)):
return .room(roomID: roomID) return .room(roomID: roomID)
default: default:
return nil return nil
@@ -211,9 +211,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
presentMessageForwarding(for: eventID) presentMessageForwarding(for: eventID)
case (.messageForwarding, .dismissMessageForwarding, .room): case (.messageForwarding, .dismissMessageForwarding, .room):
break break
case (.room, .presentLocationPicker, .locationPicker): case (.room, .presentMapNavigator(let mode), .mapNavigator):
presentLocationPicker() presentMapNavigator(interactionMode: mode)
case (.locationPicker, .dismissLocationPicker, .room): case (.mapNavigator, .dismissMapNavigator, .room):
break break
default: default:
fatalError("Unknown transition: \(context)") fatalError("Unknown transition: \(context)")
@@ -307,7 +307,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .presentEmojiPicker(let itemID): case .presentEmojiPicker(let itemID):
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID)) stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID))
case .presentLocationPicker: case .presentLocationPicker:
stateMachine.tryEvent(.presentLocationPicker) stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker))
case .presentLocationViewer(_, let geoURI):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI)))
case .presentRoomMemberDetails(member: let member): case .presentRoomMemberDetails(member: let member):
stateMachine.tryEvent(.presentRoomMemberDetails(member: .init(value: member))) stateMachine.tryEvent(.presentRoomMemberDetails(member: .init(value: member)))
case .presentMessageForwarding(let itemID): case .presentMessageForwarding(let itemID):
@@ -500,10 +502,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
private func presentLocationPicker() { private func presentMapNavigator(interactionMode: StaticLocationInteractionMode) {
let locationPickerNavigationStackCoordinator = NavigationStackCoordinator() let locationPickerNavigationStackCoordinator = NavigationStackCoordinator()
let params = StaticLocationScreenCoordinatorParameters() let params = StaticLocationScreenCoordinatorParameters(interactionMode: interactionMode)
let coordinator = StaticLocationScreenCoordinator(parameters: params) let coordinator = StaticLocationScreenCoordinator(parameters: params)
coordinator.actions.sink { [weak self] action in coordinator.actions.sink { [weak self] action in
@@ -519,11 +521,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
locationPickerNavigationStackCoordinator.setRootCoordinator(coordinator) locationPickerNavigationStackCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(locationPickerNavigationStackCoordinator) { [weak self] in navigationStackCoordinator.setSheetCoordinator(locationPickerNavigationStackCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissLocationPicker) self?.stateMachine.tryEvent(.dismissMapNavigator)
} }
} }
@@ -618,7 +620,7 @@ private extension RoomFlowCoordinator {
case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource) case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource)
case mediaUploadPreview(roomID: String, fileURL: URL) case mediaUploadPreview(roomID: String, fileURL: URL)
case emojiPicker(roomID: String, itemID: String) case emojiPicker(roomID: String, itemID: String)
case locationPicker(roomID: String) case mapNavigator(roomID: String)
case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper) case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper)
case messageForwarding(roomID: String, itemID: String) case messageForwarding(roomID: String, itemID: String)
} }
@@ -647,8 +649,8 @@ private extension RoomFlowCoordinator {
case presentEmojiPicker(itemID: String) case presentEmojiPicker(itemID: String)
case dismissEmojiPicker case dismissEmojiPicker
case presentLocationPicker case presentMapNavigator(interactionMode: StaticLocationInteractionMode)
case dismissLocationPicker case dismissMapNavigator
case presentRoomMemberDetails(member: HashableRoomMemberWrapper) case presentRoomMemberDetails(member: HashableRoomMemberWrapper)
case dismissRoomMemberDetails case dismissRoomMemberDetails

View File

@@ -804,6 +804,12 @@ public enum L10n {
public static var screenShareLocationTitle: String { return L10n.tr("Localizable", "screen_share_location_title") } public static var screenShareLocationTitle: String { return L10n.tr("Localizable", "screen_share_location_title") }
/// Share my location /// Share my location
public static var screenShareMyLocationAction: String { return L10n.tr("Localizable", "screen_share_my_location_action") } public static var screenShareMyLocationAction: String { return L10n.tr("Localizable", "screen_share_my_location_action") }
/// Open in Apple Maps
public static var screenShareOpenAppleMaps: String { return L10n.tr("Localizable", "screen_share_open_apple_maps") }
/// Open in Google Maps
public static var screenShareOpenGoogleMaps: String { return L10n.tr("Localizable", "screen_share_open_google_maps") }
/// Open in OpenStreetMap
public static var screenShareOpenOsmMaps: String { return L10n.tr("Localizable", "screen_share_open_osm_maps") }
/// Share this location /// Share this location
public static var screenShareThisLocationAction: String { return L10n.tr("Localizable", "screen_share_this_location_action") } public static var screenShareThisLocationAction: String { return L10n.tr("Localizable", "screen_share_this_location_action") }
/// Are you sure you want to sign out? /// Are you sure you want to sign out?
@@ -818,6 +824,8 @@ public enum L10n {
public static var screenSignoutPreferenceItem: String { return L10n.tr("Localizable", "screen_signout_preference_item") } public static var screenSignoutPreferenceItem: String { return L10n.tr("Localizable", "screen_signout_preference_item") }
/// An error occurred when trying to start a chat /// An error occurred when trying to start a chat
public static var screenStartChatErrorStartingChat: String { return L10n.tr("Localizable", "screen_start_chat_error_starting_chat") } public static var screenStartChatErrorStartingChat: String { return L10n.tr("Localizable", "screen_start_chat_error_starting_chat") }
/// Location
public static var screenViewLocationTitle: String { return L10n.tr("Localizable", "screen_view_location_title") }
/// There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again. /// There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again.
/// ///
/// Thanks for your patience! /// Thanks for your patience!

View File

@@ -18,83 +18,42 @@ import Foundation
import Mapbox import Mapbox
import SwiftUI import SwiftUI
/// Base class to handle a map annotation final class LocationAnnotation: NSObject, MGLAnnotation {
class LocationAnnotation: NSObject, MGLAnnotation {
// MARK: - Properties
// Title property is needed to enable annotation selection and callout view showing
var title: String?
let coordinate: CLLocationCoordinate2D let coordinate: CLLocationCoordinate2D
let anchorPoint: CGPoint
let view: AnyView
// MARK: - Setup // MARK: - Setup
init(coordinate: CLLocationCoordinate2D) { init(coordinate: CLLocationCoordinate2D,
anchorPoint: CGPoint = .init(x: 0.5, y: 0.5),
@ViewBuilder label: () -> some View) {
self.coordinate = coordinate self.coordinate = coordinate
self.anchorPoint = anchorPoint
view = AnyView(label())
super.init() super.init()
} }
} }
/// POI map annotation final class LocationAnnotationView: MGLUserLocationAnnotationView {
class PinLocationAnnotation: LocationAnnotation { }
class LocationAnnotationView: MGLUserLocationAnnotationView {
private enum Constants {
static let defaultFrame = CGRect(x: 0, y: 0, width: 46, height: 46)
}
// MARK: - Setup // MARK: - Setup
override init(annotation: MGLAnnotation?, reuseIdentifier: String?) { override init(annotation: MGLAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: super.init(annotation: annotation, reuseIdentifier:
reuseIdentifier) reuseIdentifier)
frame = Constants.defaultFrame
} }
convenience init(userPinLocationAnnotation: MGLAnnotation) { convenience init(annotation: LocationAnnotation) {
self.init(annotation: userPinLocationAnnotation, reuseIdentifier: "userPinLocation") self.init(annotation: annotation, reuseIdentifier: "\(Self.self)")
let view: UIView = UIHostingController(rootView: annotation.view).view
addUserView() view.backgroundColor = .clear
} view.anchorPoint = annotation.anchorPoint
addSubview(view)
convenience init(pinLocationAnnotation: PinLocationAnnotation) { view.bounds.size = view.intrinsicContentSize
self.init(annotation: pinLocationAnnotation, reuseIdentifier: nil)
addPinView()
} }
@available(*, unavailable) @available(*, unavailable)
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError() fatalError()
} }
// MARK: - Private
private func addUserView() {
guard let pinView = UIHostingController(rootView: Image(systemName: "circle.fill")
.resizable()
.foregroundColor(.compound.iconPrimary)).view else {
return
}
addMarkerView(pinView)
}
private func addPinView() {
guard let pinView = UIHostingController(rootView: Image(systemName: "mappin")
.resizable()
.foregroundColor(.compound.iconPrimary)).view else {
return
}
addMarkerView(pinView)
}
private func addMarkerView(_ markerView: UIView) {
markerView.backgroundColor = .clear
addSubview(markerView)
markerView.frame = bounds
}
} }

View File

@@ -19,11 +19,19 @@ import Mapbox
import SwiftUI import SwiftUI
struct MapLibreMapView: UIViewRepresentable { struct MapLibreMapView: UIViewRepresentable {
// MARK: - Constants struct Options {
/// The initial zoom level
private enum Constants { let zoomLevel: Double
static let mapZoomLevel = 15.0 /// The initial map center
static let mapZoomLevelWithoutPermission = 5.0 let mapCenter: CLLocationCoordinate2D?
/// Map annotations
let annotations: [LocationAnnotation]
init(zoomLevel: Double, mapCenter: CLLocationCoordinate2D? = nil, annotations: [LocationAnnotation] = []) {
self.zoomLevel = zoomLevel
self.mapCenter = mapCenter
self.annotations = annotations
}
} }
// MARK: - Properties // MARK: - Properties
@@ -31,6 +39,8 @@ struct MapLibreMapView: UIViewRepresentable {
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
let builder: MapTilerStyleBuilderProtocol let builder: MapTilerStyleBuilderProtocol
let options: Options
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
var showsUserLocationMode: ShowUserLocationMode = .hide var showsUserLocationMode: ShowUserLocationMode = .hide
@@ -52,12 +62,13 @@ struct MapLibreMapView: UIViewRepresentable {
let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.didPan)) let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.didPan))
panGesture.delegate = context.coordinator panGesture.delegate = context.coordinator
mapView.addGestureRecognizer(panGesture) mapView.addGestureRecognizer(panGesture)
mapView.zoomLevel = Constants.mapZoomLevelWithoutPermission setupMap(mapView: mapView, with: options)
return mapView return mapView
} }
func updateUIView(_ mapView: MGLMapView, context: Context) { func updateUIView(_ mapView: MGLMapView, context: Context) {
mapView.removeAllAnnotations() mapView.removeAllAnnotations()
mapView.addAnnotations(options.annotations)
if colorScheme == .dark { if colorScheme == .dark {
mapView.styleURL = builder.dynamicMapURL(for: .dark) mapView.styleURL = builder.dynamicMapURL(for: .dark)
@@ -73,6 +84,13 @@ struct MapLibreMapView: UIViewRepresentable {
} }
// MARK: - Private // MARK: - Private
private func setupMap(mapView: MGLMapView, with options: Options) {
mapView.zoomLevel = options.zoomLevel
if let mapCenter = options.mapCenter {
mapView.centerCoordinate = mapCenter
}
}
private func makeMapView() -> MGLMapView { private func makeMapView() -> MGLMapView {
let mapView = MGLMapView(frame: .zero, styleURL: colorScheme == .dark ? builder.dynamicMapURL(for: .dark) : builder.dynamicMapURL(for: .light)) let mapView = MGLMapView(frame: .zero, styleURL: colorScheme == .dark ? builder.dynamicMapURL(for: .dark) : builder.dynamicMapURL(for: .light))
@@ -85,7 +103,7 @@ struct MapLibreMapView: UIViewRepresentable {
private func showUserLocation(in mapView: MGLMapView) { private func showUserLocation(in mapView: MGLMapView) {
switch showsUserLocationMode { switch showsUserLocationMode {
case .follow: case .showAndFollow:
mapView.showsUserLocation = true mapView.showsUserLocation = true
mapView.userTrackingMode = .follow mapView.userTrackingMode = .follow
case .show: case .show:
@@ -115,12 +133,10 @@ extension MapLibreMapView {
// MARK: - MGLMapViewDelegate // MARK: - MGLMapViewDelegate
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if let pinLocationAnnotation = annotation as? PinLocationAnnotation { guard let annotation = annotation as? LocationAnnotation else {
return LocationAnnotationView(pinLocationAnnotation: pinLocationAnnotation) return nil
} else if annotation is MGLUserLocation {
return LocationAnnotationView(userPinLocationAnnotation: annotation)
} }
return nil return LocationAnnotationView(annotation: annotation)
} }
func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) { func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) {
@@ -143,7 +159,10 @@ extension MapLibreMapView {
} }
func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) { func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) {
mapLibreView.mapCenterCoordinate = mapView.centerCoordinate // Fixes: "Publishing changes from within view updates is not allowed, this will cause undefined behavior."
DispatchQueue.main.async { [mapLibreView] in
mapLibreView.mapCenterCoordinate = mapView.centerCoordinate
}
} }
// MARK: Callout // MARK: Callout

View File

@@ -20,10 +20,10 @@ import Foundation
Behavior mode of the current user's location, can be hidden, only shown and shown following the user Behavior mode of the current user's location, can be hidden, only shown and shown following the user
*/ */
enum ShowUserLocationMode { enum ShowUserLocationMode {
/// this mode will show the user pin in map and track him, panning the map automatically
case follow
/// this mode will show the user pin in map /// this mode will show the user pin in map
case show case show
/// this mode will show the user pin in map and track him, panning the map automatically
case showAndFollow
/// this mode will not show the user pin in map /// this mode will not show the user pin in map
case hide case hide
} }

View File

@@ -0,0 +1,92 @@
//
// Copyright 2023 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 CoreLocation
import UIKit
final class ShareToMapsAppActivity: UIActivity {
enum MapsAppType: CaseIterable {
case apple
case google
case osm
}
private let type: MapsAppType
private let location: CLLocationCoordinate2D
init(type: MapsAppType, location: CLLocationCoordinate2D) {
self.type = type
self.location = location
super.init()
}
override private init() {
fatalError()
}
override var activityTitle: String? {
type.activityTitle
}
var activityCategory: UIActivity.Category {
.action
}
override var activityType: UIActivity.ActivityType {
.shareToMapsApp
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
true
}
override func prepare(withActivityItems activityItems: [Any]) {
UIApplication.shared.open(type.activityURL(for: location), options: [:]) { [weak self] result in
self?.activityDidFinish(result)
}
}
}
extension ShareToMapsAppActivity.MapsAppType {
func activityURL(for location: CLLocationCoordinate2D) -> URL {
switch self {
case .apple:
// swiftlint:disable:next force_unwrapping
return URL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)&q=Pin")!
case .google:
// swiftlint:disable:next force_unwrapping
return URL(string: "https://www.google.com/maps/search/?api=1&query=\(location.latitude),\(location.longitude)")!
case .osm:
// swiftlint:disable:next force_unwrapping
return URL(string: "https://www.openstreetmap.org/?mlat=\(location.latitude)&mlon=\(location.longitude)")!
}
}
var activityTitle: String {
switch self {
case .apple:
return L10n.screenShareOpenAppleMaps
case .google:
return L10n.screenShareOpenGoogleMaps
case .osm:
return L10n.screenShareOpenOsmMaps
}
}
}
private extension UIActivity.ActivityType {
static let shareToMapsApp = UIActivity.ActivityType("ElementX.ShareToMapsApp")
}

View File

@@ -0,0 +1,65 @@
//
// Copyright 2023 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
struct AppActivityView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIActivityViewController
typealias CompletionType = (Result<(activity: UIActivity.ActivityType, items: [Any]?), Error>) -> Void
private let activityItems: [Any]
private let applicationActivities: [UIActivity]?
private var excludedActivityTypes: [UIActivity.ActivityType]
private var onCancel: (() -> Void)?
private var onComplete: CompletionType?
public init(activityItems: [Any],
applicationActivities: [UIActivity]? = nil,
excludedActivityTypes: [UIActivity.ActivityType] = [],
onCancel: (() -> Void)? = nil,
onComplete: CompletionType? = nil) {
self.activityItems = activityItems
self.applicationActivities = applicationActivities
self.excludedActivityTypes = excludedActivityTypes
self.onCancel = onCancel
self.onComplete = onComplete
}
public func makeUIViewController(context: Context) -> UIViewControllerType {
let viewController = UIViewControllerType(activityItems: activityItems, applicationActivities: applicationActivities)
viewController.excludedActivityTypes = excludedActivityTypes
viewController.completionWithItemsHandler = { activity, completed, items, error in
if let error {
onComplete?(.failure(error))
} else if let activity, completed {
onComplete?(.success((activity, items)))
} else if !completed {
onCancel?()
} else {
assertionFailure()
}
}
return viewController
}
public func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
public static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Coordinator) {
uiViewController.completionWithItemsHandler = nil
}
}

View File

@@ -27,13 +27,90 @@ enum StaticLocationScreenViewModelAction {
case sendLocation(GeoURI) case sendLocation(GeoURI)
} }
enum StaticLocationInteractionMode: Hashable {
case picker
case viewOnly(geoURI: GeoURI, description: String? = nil)
}
struct StaticLocationScreenViewState: BindableState { struct StaticLocationScreenViewState: BindableState {
init(interactionMode: StaticLocationInteractionMode, isPinDropSharing: Bool = true, showsUserLocationMode: ShowUserLocationMode = .hide) {
self.interactionMode = interactionMode
self.isPinDropSharing = isPinDropSharing
self.showsUserLocationMode = showsUserLocationMode
switch interactionMode {
case .picker:
bindings = .init()
case .viewOnly(let geoURI, _):
bindings = .init(mapCenterLocation: .init(latitude: geoURI.latitude, longitude: geoURI.longitude))
}
}
let interactionMode: StaticLocationInteractionMode
/// Indicates whether the user has moved around the map to drop a pin somewhere other than their current location /// Indicates whether the user has moved around the map to drop a pin somewhere other than their current location
var isPinDropSharing = true var isPinDropSharing = true
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user /// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
var showsUserLocationMode: ShowUserLocationMode = .hide var showsUserLocationMode: ShowUserLocationMode = .hide
var bindings = StaticLocationScreenBindings() var bindings = StaticLocationScreenBindings()
var showBottomToolbar: Bool {
interactionMode == .picker
}
var mapAnnotationCoordinate: CLLocationCoordinate2D? {
switch interactionMode {
case .picker:
return nil
case .viewOnly(let geoURI, _):
return .init(latitude: geoURI.latitude, longitude: geoURI.longitude)
}
}
var showPinInTheCenter: Bool {
switch interactionMode {
case .picker:
return isPinDropSharing
case .viewOnly:
return false
}
}
var navigationTitle: String {
switch interactionMode {
case .picker:
return L10n.screenShareLocationTitle
case .viewOnly:
return L10n.screenViewLocationTitle
}
}
var showShareAction: Bool {
switch interactionMode {
case .picker:
return false
case .viewOnly:
return true
}
}
var zoomLevel: Double {
switch interactionMode {
case .picker:
return 5.0
case .viewOnly:
return 15.0
}
}
var locationDescription: String? {
switch interactionMode {
case .picker:
return nil
case .viewOnly(_, let description):
return description
}
}
} }
struct StaticLocationScreenBindings { struct StaticLocationScreenBindings {
@@ -54,6 +131,8 @@ struct StaticLocationScreenBindings {
/// Information describing the currently displayed alert. /// Information describing the currently displayed alert.
var alertInfo: AlertInfo<LocationSharingViewError>? var alertInfo: AlertInfo<LocationSharingViewError>?
var showShareSheet = false
} }
enum StaticLocationScreenViewAction { enum StaticLocationScreenViewAction {

View File

@@ -17,7 +17,9 @@
import Combine import Combine
import SwiftUI import SwiftUI
struct StaticLocationScreenCoordinatorParameters { } struct StaticLocationScreenCoordinatorParameters {
let interactionMode: StaticLocationInteractionMode
}
enum StaticLocationScreenCoordinatorAction { enum StaticLocationScreenCoordinatorAction {
case close case close
@@ -38,7 +40,7 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
init(parameters: StaticLocationScreenCoordinatorParameters) { init(parameters: StaticLocationScreenCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
viewModel = StaticLocationScreenViewModel() viewModel = StaticLocationScreenViewModel(interactionMode: parameters.interactionMode)
} }
// MARK: - Public // MARK: - Public

View File

@@ -26,8 +26,8 @@ class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLo
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
} }
init() { init(interactionMode: StaticLocationInteractionMode) {
super.init(initialViewState: StaticLocationScreenViewState()) super.init(initialViewState: .init(interactionMode: interactionMode))
} }
override func process(viewAction: StaticLocationScreenViewAction) { override func process(viewAction: StaticLocationScreenViewAction) {

View File

@@ -22,43 +22,81 @@ struct StaticLocationScreen: View {
private let builder = MapTilerStyleBuilder(appSettings: ServiceLocator.shared.settings) private let builder = MapTilerStyleBuilder(appSettings: ServiceLocator.shared.settings)
var body: some View { var body: some View {
mapView VStack(spacing: 0) {
.ignoresSafeArea(.all, edges: .horizontal) if let locationDescription = context.viewState.locationDescription {
.navigationTitle(L10n.screenShareLocationTitle) Text(locationDescription)
.navigationBarTitleDisplayMode(.inline) .lineLimit(2)
.toolbar { toolbar } .foregroundColor(Color.compound.textPrimary)
.alert(item: $context.alertInfo) .font(.compound.bodyMD)
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
mapView
}
.navigationTitle(context.viewState.navigationTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.alert(item: $context.alertInfo)
} }
private var mapView: some View { private var mapView: some View {
ZStack(alignment: .center) { ZStack(alignment: .center) {
MapLibreMapView(builder: builder, MapLibreMapView(builder: builder,
options: mapOptions,
showsUserLocationMode: .hide, showsUserLocationMode: .hide,
error: $context.mapError, error: $context.mapError,
mapCenterCoordinate: $context.mapCenterLocation, mapCenterCoordinate: $context.mapCenterLocation,
userDidPan: { userDidPan: {
context.send(viewAction: .userDidPan) context.send(viewAction: .userDidPan)
}) })
if context.viewState.isPinDropSharing { if context.viewState.showPinInTheCenter {
LocationMarkerView() LocationMarkerView()
} }
} }
.ignoresSafeArea(.all, edges: mapSafeAreaEdges)
} }
// MARK: - Private
@ToolbarContentBuilder @ToolbarContentBuilder
private var toolbar: some ToolbarContent { private var toolbar: some ToolbarContent {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
closeButton closeButton
} }
ToolbarItemGroup(placement: .bottomBar) { if context.viewState.showShareAction {
shareLocationButton ToolbarItem(placement: .navigationBarTrailing) {
Spacer() shareButton
.popover(isPresented: $context.showShareSheet) { shareSheet }
}
} }
if context.viewState.showBottomToolbar {
ToolbarItemGroup(placement: .bottomBar) {
selectLocationButton
Spacer()
}
}
}
private var mapOptions: MapLibreMapView.Options {
guard let coordinate = context.viewState.mapAnnotationCoordinate else {
return .init(zoomLevel: context.viewState.zoomLevel)
}
return .init(zoomLevel: context.viewState.zoomLevel,
mapCenter: coordinate,
annotations: [LocationAnnotation(coordinate: coordinate, anchorPoint: .bottomCenter) {
LocationMarkerView()
}])
}
private var mapSafeAreaEdges: Edge.Set {
context.viewState.showBottomToolbar ? .horizontal : [.horizontal, .bottom]
} }
@ScaledMetric private var shareMarkerSize: CGFloat = 28 @ScaledMetric private var shareMarkerSize: CGFloat = 28
private var shareLocationButton: some View { private var selectLocationButton: some View {
Button { Button {
context.send(viewAction: .selectLocation) context.send(viewAction: .selectLocation)
} label: { } label: {
@@ -73,25 +111,52 @@ struct StaticLocationScreen: View {
} }
private var closeButton: some View { private var closeButton: some View {
Button(L10n.actionCancel, action: close) Button(L10n.actionCancel) {
context.send(viewAction: .close)
}
} }
private func close() { private var shareButton: some View {
context.send(viewAction: .close) Button {
context.showShareSheet = true
} label: {
Image(systemName: "square.and.arrow.up")
}
}
@ViewBuilder
private var shareSheet: some View {
if let location = context.viewState.mapAnnotationCoordinate {
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location)],
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location) })
.edgesIgnoringSafeArea(.bottom)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.hidden)
}
} }
} }
// MARK: - Previews // MARK: - Previews
struct StaticLocationScreenViewer_Previews: PreviewProvider { struct StaticLocationScreenViewer_Previews: PreviewProvider {
static let viewModel = {
let viewModel = StaticLocationScreenViewModel()
return viewModel
}()
static var previews: some View { static var previews: some View {
NavigationView { NavigationStack {
StaticLocationScreen(context: viewModel.context) StaticLocationScreen(context: StaticLocationScreenViewModel(interactionMode: .picker).context)
} }
.previewDisplayName("Picker")
NavigationStack {
StaticLocationScreen(context: StaticLocationScreenViewModel(interactionMode: .viewOnly(geoURI: .init(latitude: 41.9027835, longitude: 12.4963655))).context)
}
.previewDisplayName("View Only")
NavigationStack {
StaticLocationScreen(context: StaticLocationScreenViewModel(interactionMode: .viewOnly(geoURI: .init(latitude: 41.9027835, longitude: 12.4963655), description: "Cool position")).context)
}
.previewDisplayName("View Only (with description)")
} }
} }
private extension CGPoint {
static let bottomCenter: Self = .init(x: 0.5, y: 1)
}

View File

@@ -30,6 +30,7 @@ enum RoomScreenCoordinatorAction {
case presentMediaUploadPreviewScreen(URL) case presentMediaUploadPreviewScreen(URL)
case presentRoomDetails case presentRoomDetails
case presentLocationPicker case presentLocationPicker
case presentLocationViewer(body: String, geoURI: GeoURI)
case presentEmojiPicker(itemID: String) case presentEmojiPicker(itemID: String)
case presentRoomMemberDetails(member: RoomMemberProxyProtocol) case presentRoomMemberDetails(member: RoomMemberProxyProtocol)
case presentMessageForwarding(itemID: String) case presentMessageForwarding(itemID: String)
@@ -84,6 +85,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentRoomMemberDetails(member: member)) actionsSubject.send(.presentRoomMemberDetails(member: member))
case .displayMessageForwarding(let itemID): case .displayMessageForwarding(let itemID):
actionsSubject.send(.presentMessageForwarding(itemID: itemID)) actionsSubject.send(.presentMessageForwarding(itemID: itemID))
case .displayLocation(let body, let geoURI):
actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI))
} }
} }
} }

View File

@@ -29,6 +29,7 @@ enum RoomScreenViewModelAction {
case displayMediaUploadPreviewScreen(url: URL) case displayMediaUploadPreviewScreen(url: URL)
case displayRoomMemberDetails(member: RoomMemberProxyProtocol) case displayRoomMemberDetails(member: RoomMemberProxyProtocol)
case displayMessageForwarding(itemID: String) case displayMessageForwarding(itemID: String)
case displayLocation(body: String, geoURI: GeoURI)
} }
enum RoomScreenComposerMode: Equatable { enum RoomScreenComposerMode: Equatable {

View File

@@ -221,6 +221,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
switch action { switch action {
case .displayMediaFile(let file, let title): case .displayMediaFile(let file, let title):
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: title) state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: title)
case .displayLocation(let body, let geoURI):
callback?(.displayLocation(body: body, geoURI: geoURI))
case .none: case .none:
break break
} }

View File

@@ -41,7 +41,6 @@ enum RoomProxyError: Error {
case failedSettingRoomTopic case failedSettingRoomTopic
case failedRemovingAvatar case failedRemovingAvatar
case failedUploadingAvatar case failedUploadingAvatar
case failedSendingLocation
} }
@MainActor @MainActor

View File

@@ -43,9 +43,9 @@ struct GeoURI: Hashable {
var string: String { var string: String {
if let uncertainty { if let uncertainty {
return "geo:\(latitude),\(longitude);u=\(uncertainty)" return "geo:\(string(for: latitude)),\(string(for: longitude));u=\(string(for: uncertainty))"
} else { } else {
return "geo:\(latitude),\(longitude)" return "geo:\(string(for: latitude)),\(string(for: longitude))"
} }
} }
@@ -64,6 +64,10 @@ struct GeoURI: Hashable {
let uncertainty = matchOutput.uncertainty.flatMap(Double.init) let uncertainty = matchOutput.uncertainty.flatMap(Double.init)
return .init(latitude: latitude, longitude: longitude, uncertainty: uncertainty) return .init(latitude: latitude, longitude: longitude, uncertainty: uncertainty)
} }
private func string(for number: Double) -> String {
NumberFormatter.decimal.string(from: .init(floatLiteral: number)) ?? "\(number)"
}
} }
// swiftlint:disable:next large_tuple // swiftlint:disable:next large_tuple
@@ -78,3 +82,13 @@ extension GeoURI {
self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) self.init(latitude: coordinate.latitude, longitude: coordinate.longitude)
} }
} }
private extension NumberFormatter {
static let decimal: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.locale = Locale(identifier: "en_US_POSIX")
numberFormatter.numberStyle = .decimal
numberFormatter.maximumFractionDigits = 30
return numberFormatter
}()
}

View File

@@ -115,32 +115,12 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return .none return .none
} }
var source: MediaSourceProxy?
var body: String
switch timelineItem { switch timelineItem {
case let item as ImageRoomTimelineItem: case let item as LocationRoomTimelineItem:
source = item.content.source guard let geoURI = item.content.geoURI else { return .none }
body = item.content.body return .displayLocation(body: item.content.body, geoURI: geoURI)
case let item as VideoRoomTimelineItem:
source = item.content.source
body = item.content.body
case let item as FileRoomTimelineItem:
source = item.content.source
body = item.content.body
case let item as AudioRoomTimelineItem:
// For now we are just displaying audio messages with the File preview until we create a timeline player for them.
source = item.content.source
body = item.content.body
default: default:
return .none return await displayMediaActionIfPossible(timelineItem: timelineItem)
}
guard let source else { return .none }
switch await mediaProvider.loadFileFromSource(source, body: body) {
case .success(let file):
return .displayMediaFile(file: file, title: body)
case .failure:
return .none
} }
} }
@@ -221,6 +201,37 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// Recompute all attributed strings on content size changes -> DynamicType support // Recompute all attributed strings on content size changes -> DynamicType support
updateTimelineItems() updateTimelineItems()
} }
private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> RoomTimelineControllerAction {
var source: MediaSourceProxy?
var body: String
switch timelineItem {
case let item as ImageRoomTimelineItem:
source = item.content.source
body = item.content.body
case let item as VideoRoomTimelineItem:
source = item.content.source
body = item.content.body
case let item as FileRoomTimelineItem:
source = item.content.source
body = item.content.body
case let item as AudioRoomTimelineItem:
// For now we are just displaying audio messages with the File preview until we create a timeline player for them.
source = item.content.source
body = item.content.body
default:
return .none
}
guard let source else { return .none }
switch await mediaProvider.loadFileFromSource(source, body: body) {
case .success(let file):
return .displayMediaFile(file: file, title: body)
case .failure:
return .none
}
}
private func updateTimelineItems() { private func updateTimelineItems() {
var newTimelineItems = [RoomTimelineItemProtocol]() var newTimelineItems = [RoomTimelineItemProtocol]()

View File

@@ -26,6 +26,7 @@ enum RoomTimelineControllerCallback {
enum RoomTimelineControllerAction { enum RoomTimelineControllerAction {
case displayMediaFile(file: MediaFileHandleProxy, title: String?) case displayMediaFile(file: MediaFileHandleProxy, title: String?)
case displayLocation(body: String, geoURI: GeoURI)
case none case none
} }

View File

@@ -19,36 +19,36 @@ import XCTest
final class GeoURITests: XCTestCase { final class GeoURITests: XCTestCase {
func testValidPositiveCoordinates() throws { func testValidPositiveCoordinates() throws {
let string = "geo:53.99803101552848,8.25347900390625;u=10.123" let string = "geo:53.9980310155285,8.25347900390625;u=10.123"
let uri = try XCTUnwrap(GeoURI(string: string)) let uri = try XCTUnwrap(GeoURI(string: string))
XCTAssertEqual(uri.latitude, 53.99803101552848) XCTAssertEqual(uri.latitude, 53.9980310155285)
XCTAssertEqual(uri.longitude, 8.25347900390625) XCTAssertEqual(uri.longitude, 8.25347900390625)
XCTAssertEqual(uri.uncertainty, 10.123) XCTAssertEqual(uri.uncertainty, 10.123)
XCTAssertEqual(uri.string, string) XCTAssertEqual(uri.string, string)
} }
func testValidNegativeCoordinates() throws { func testValidNegativeCoordinates() throws {
let string = "geo:-53.99803101552848,-8.25347900390625;u=10.0" let string = "geo:-53.9980310155285,-8.25347900390625;u=10"
let uri = try XCTUnwrap(GeoURI(string: string)) let uri = try XCTUnwrap(GeoURI(string: string))
XCTAssertEqual(uri.latitude, -53.99803101552848) XCTAssertEqual(uri.latitude, -53.9980310155285)
XCTAssertEqual(uri.longitude, -8.25347900390625) XCTAssertEqual(uri.longitude, -8.25347900390625)
XCTAssertEqual(uri.uncertainty, 10) XCTAssertEqual(uri.uncertainty, 10)
XCTAssertEqual(uri.string, string) XCTAssertEqual(uri.string, string)
} }
func testValidMixedCoordinates() throws { func testValidMixedCoordinates() throws {
let string = "geo:53.99803101552848,-8.25347900390625;u=10.0" let string = "geo:53.9980310155285,-8.25347900390625;u=10"
let uri = try XCTUnwrap(GeoURI(string: string)) let uri = try XCTUnwrap(GeoURI(string: string))
XCTAssertEqual(uri.latitude, 53.99803101552848) XCTAssertEqual(uri.latitude, 53.9980310155285)
XCTAssertEqual(uri.longitude, -8.25347900390625) XCTAssertEqual(uri.longitude, -8.25347900390625)
XCTAssertEqual(uri.uncertainty, 10) XCTAssertEqual(uri.uncertainty, 10)
XCTAssertEqual(uri.string, string) XCTAssertEqual(uri.string, string)
} }
func testValidCoordinatesNoUncertainty() throws { func testValidCoordinatesNoUncertainty() throws {
let string = "geo:53.99803101552848,-8.25347900390625" let string = "geo:53.9980310155285,-8.25347900390625"
let uri = try XCTUnwrap(GeoURI(string: string)) let uri = try XCTUnwrap(GeoURI(string: string))
XCTAssertEqual(uri.latitude, 53.99803101552848) XCTAssertEqual(uri.latitude, 53.9980310155285)
XCTAssertEqual(uri.longitude, -8.25347900390625) XCTAssertEqual(uri.longitude, -8.25347900390625)
XCTAssertNil(uri.uncertainty) XCTAssertNil(uri.uncertainty)
XCTAssertEqual(uri.string, string) XCTAssertEqual(uri.string, string)
@@ -60,7 +60,12 @@ final class GeoURITests: XCTestCase {
XCTAssertEqual(uri.latitude, 53) XCTAssertEqual(uri.latitude, 53)
XCTAssertEqual(uri.longitude, -8) XCTAssertEqual(uri.longitude, -8)
XCTAssertEqual(uri.uncertainty, 35) XCTAssertEqual(uri.uncertainty, 35)
XCTAssertEqual(uri.string, "geo:53.0,-8.0;u=35.0") XCTAssertEqual(uri.string, "geo:53,-8;u=35")
}
func testFormattingExponentialNotation() throws {
let uri = GeoURI(latitude: 1e2, longitude: -1e-2, uncertainty: 1e-4)
XCTAssertEqual(uri.string, "geo:100,-0.01;u=0.0001")
} }
func testInvalidURI1() { func testInvalidURI1() {

View File

@@ -31,7 +31,7 @@ class StaticLocationScreenViewModelTests: XCTestCase {
} }
override func setUpWithError() throws { override func setUpWithError() throws {
let viewModel = StaticLocationScreenViewModel() let viewModel = StaticLocationScreenViewModel(interactionMode: .picker)
viewModel.state.isPinDropSharing = false viewModel.state.isPinDropSharing = false
self.viewModel = viewModel self.viewModel = viewModel
} }