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:
@@ -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 */,
|
||||||
|
|||||||
@@ -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!";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
92
ElementX/Sources/Other/ShareToMapsAppActivity.swift
Normal file
92
ElementX/Sources/Other/ShareToMapsAppActivity.swift
Normal 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")
|
||||||
|
}
|
||||||
65
ElementX/Sources/Other/SwiftUI/Views/AppActivityView.swift
Normal file
65
ElementX/Sources/Other/SwiftUI/Views/AppActivityView.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ enum RoomProxyError: Error {
|
|||||||
case failedSettingRoomTopic
|
case failedSettingRoomTopic
|
||||||
case failedRemovingAvatar
|
case failedRemovingAvatar
|
||||||
case failedUploadingAvatar
|
case failedUploadingAvatar
|
||||||
case failedSendingLocation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
@@ -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
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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]()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user