mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:06:00 +00:00
Render LiveLocations in the map (#5394)
This commit is contained in:
@@ -26,6 +26,7 @@ enum LocationSharingScreenViewModelAction {
|
||||
enum LocationSharingInteractionMode: Hashable {
|
||||
case picker
|
||||
case viewStatic(StaticLocationData)
|
||||
case viewLive(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare)
|
||||
}
|
||||
|
||||
struct LocationSharingScreenViewState: BindableState {
|
||||
@@ -38,16 +39,23 @@ struct LocationSharingScreenViewState: BindableState {
|
||||
self.showLiveLocationSharingButton = showLiveLocationSharingButton
|
||||
self.ownUserID = ownUserID
|
||||
|
||||
userProfile = switch interactionMode {
|
||||
let initialProfile: UserProfileProxy = switch interactionMode {
|
||||
case .viewStatic(let locationData):
|
||||
.init(sender: locationData.sender)
|
||||
case .viewLive(let sender, _):
|
||||
.init(sender: sender)
|
||||
case .picker:
|
||||
.init(userID: ownUserID)
|
||||
}
|
||||
userProfiles = [initialProfile.userID: initialProfile]
|
||||
|
||||
if case .viewLive(_, let initialLiveLocationShare) = interactionMode {
|
||||
liveLocationShares = [initialLiveLocationShare]
|
||||
}
|
||||
|
||||
bindings.showsUserLocationMode = switch interactionMode {
|
||||
case .picker: .showAndFollow
|
||||
case .viewStatic: .show
|
||||
case .viewStatic, .viewLive: .show
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +63,35 @@ struct LocationSharingScreenViewState: BindableState {
|
||||
let mapURLBuilder: MapTilerURLBuilderProtocol
|
||||
let showLiveLocationSharingButton: Bool
|
||||
let ownUserID: String
|
||||
var userProfile: UserProfileProxy
|
||||
var userProfiles: [String: UserProfileProxy]
|
||||
var liveLocationShares: [LiveLocationShare] = []
|
||||
|
||||
var isOwnUser: Bool {
|
||||
userProfile.userID == ownUserID
|
||||
var annotations: [LocationAnnotation] {
|
||||
switch interactionMode {
|
||||
case .viewStatic(let location):
|
||||
let profile = userProfiles.values.first
|
||||
let kind: LocationMarkerKind = if location.kind == .sender, let profile {
|
||||
.staticUser(profile)
|
||||
} else {
|
||||
.pin
|
||||
}
|
||||
let coordinate = CLLocationCoordinate2D(latitude: location.geoURI.latitude, longitude: location.geoURI.longitude)
|
||||
return [LocationAnnotation(id: kind.id, coordinate: coordinate, kind: kind)]
|
||||
case .viewLive:
|
||||
return liveLocationShares.compactMap { share in
|
||||
guard let geoURI = share.geoURI else { return nil }
|
||||
let profile = userProfiles[share.userID] ?? UserProfileProxy(userID: share.userID)
|
||||
let kind = LocationMarkerKind.liveUser(profile)
|
||||
let coordinate = CLLocationCoordinate2D(latitude: geoURI.latitude, longitude: geoURI.longitude)
|
||||
return LocationAnnotation(id: profile.userID, coordinate: coordinate, kind: kind)
|
||||
}
|
||||
case .picker:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func isOwnUser(_ userID: String) -> Bool {
|
||||
userID == ownUserID
|
||||
}
|
||||
|
||||
var bindings = LocationSharingScreenBindings(showsUserLocationMode: .hide)
|
||||
@@ -75,15 +108,9 @@ struct LocationSharingScreenViewState: BindableState {
|
||||
.init(latitude: 49.843, longitude: 9.902056)
|
||||
case .viewStatic(let location):
|
||||
.init(latitude: location.geoURI.latitude, longitude: location.geoURI.longitude)
|
||||
}
|
||||
}
|
||||
|
||||
var isLocationPickerMode: Bool {
|
||||
switch interactionMode {
|
||||
case .picker:
|
||||
true
|
||||
default:
|
||||
false
|
||||
case .viewLive(_, let initialLiveLocationShare):
|
||||
.init(latitude: initialLiveLocationShare.geoURI?.latitude ?? 0,
|
||||
longitude: initialLiveLocationShare.geoURI?.longitude ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,18 +128,18 @@ struct LocationSharingScreenViewState: BindableState {
|
||||
switch interactionMode {
|
||||
case .picker:
|
||||
return 2.7
|
||||
case .viewStatic:
|
||||
case .viewStatic, .viewLive:
|
||||
return 15.0
|
||||
}
|
||||
}
|
||||
|
||||
var locationMarkerKind: LocationMarkerKind {
|
||||
switch interactionMode {
|
||||
case .picker:
|
||||
isSharingUserLocation ? .staticUser(userProfile) : .pin
|
||||
case .viewStatic(let location):
|
||||
location.kind == .sender ? .staticUser(userProfile) : .pin
|
||||
/// The marker kind used for the picker overlay (not a map annotation).
|
||||
var pickerMarkerKind: LocationMarkerKind? {
|
||||
guard case .picker = interactionMode else { return nil }
|
||||
if let profile = userProfiles.values.first {
|
||||
return isSharingUserLocation ? .staticUser(profile) : .pin
|
||||
}
|
||||
return .pin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +166,7 @@ struct LocationSharingScreenBindings {
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<LocationSharingViewAlert>?
|
||||
|
||||
var showShareSheet = false
|
||||
var sharedAnnotation: LocationAnnotation?
|
||||
}
|
||||
|
||||
enum LocationSharingScreenViewAction {
|
||||
@@ -193,7 +220,7 @@ extension AlertInfo where T == LocationSharingViewAlert {
|
||||
}
|
||||
}
|
||||
|
||||
enum LocationMarkerKind {
|
||||
enum LocationMarkerKind: Equatable {
|
||||
case pin
|
||||
case staticUser(UserProfileProxy)
|
||||
case liveUser(UserProfileProxy)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@@ -26,6 +27,8 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
|
||||
}
|
||||
|
||||
private var authorizationStatusSubscription: AnyCancellable?
|
||||
// periphery:ignore - keep alive to keep receiving updates.
|
||||
private var liveLocationService: RoomLiveLocationServiceProtocol?
|
||||
|
||||
init(interactionMode: LocationSharingInteractionMode,
|
||||
mapURLBuilder: MapTilerURLBuilderProtocol,
|
||||
@@ -50,8 +53,12 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
|
||||
ownUserID: roomProxy.ownUserID),
|
||||
mediaProvider: mediaProvider)
|
||||
|
||||
updateShownUserProfile(members: roomProxy.membersPublisher.value)
|
||||
updateUserProfiles(members: roomProxy.membersPublisher.value)
|
||||
setupSubscriptions()
|
||||
|
||||
if case .viewLive = interactionMode {
|
||||
Task { await setupLiveLocationSubscription() }
|
||||
}
|
||||
}
|
||||
|
||||
override func process(viewAction: LocationSharingScreenViewAction) {
|
||||
@@ -81,9 +88,23 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupLiveLocationSubscription() async {
|
||||
let liveLocationService = await roomProxy.makeLiveLocationService()
|
||||
self.liveLocationService = liveLocationService
|
||||
|
||||
liveLocationService.liveLocationsPublisher
|
||||
.sink { [weak self] liveLocationsShares in
|
||||
guard let self else { return }
|
||||
MXLog.info("Received live location shares update: \(liveLocationsShares.count) share(s)")
|
||||
state.liveLocationShares = liveLocationsShares
|
||||
updateUserProfiles(members: roomProxy.membersPublisher.value)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func setupSubscriptions() {
|
||||
roomProxy.membersPublisher.sink { [weak self] members in
|
||||
self?.updateShownUserProfile(members: members)
|
||||
self?.updateUserProfiles(members: members)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -95,19 +116,23 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func updateShownUserProfile(members: [RoomMemberProxyProtocol]) {
|
||||
private func updateUserProfiles(members: [RoomMemberProxyProtocol]) {
|
||||
switch state.interactionMode {
|
||||
case .picker:
|
||||
if let ownUser = members.first(where: { $0.userID == roomProxy.ownUserID }).map(UserProfileProxy.init) {
|
||||
state.userProfile = ownUser
|
||||
} else {
|
||||
state.userProfile = .init(userID: roomProxy.ownUserID)
|
||||
}
|
||||
let ownUser = members.first(where: { $0.userID == roomProxy.ownUserID }).map(UserProfileProxy.init) ?? .init(userID: roomProxy.ownUserID)
|
||||
state.userProfiles = [ownUser.userID: ownUser]
|
||||
case .viewStatic(let location):
|
||||
if let sender = members.first(where: { $0.userID == location.sender.id }).map(UserProfileProxy.init) {
|
||||
state.userProfile = sender
|
||||
} else {
|
||||
state.userProfile = .init(sender: location.sender)
|
||||
let sender = members.first(where: { $0.userID == location.sender.id }).map(UserProfileProxy.init) ?? .init(sender: location.sender)
|
||||
state.userProfiles = [sender.userID: sender]
|
||||
case .viewLive(let sender, _):
|
||||
var userIDs = Set(state.liveLocationShares.map(\.userID))
|
||||
userIDs.insert(sender.id)
|
||||
state.userProfiles = userIDs.reduce(into: [:]) { dict, userID in
|
||||
if let member = members.first(where: { $0.userID == userID }) {
|
||||
dict[userID] = UserProfileProxy(member: member)
|
||||
} else {
|
||||
dict[userID] = UserProfileProxy(userID: userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import SwiftUI
|
||||
|
||||
struct LocationShareSheet: View {
|
||||
let annotation: LocationAnnotation
|
||||
|
||||
private var location: CLLocationCoordinate2D {
|
||||
annotation.coordinate
|
||||
}
|
||||
|
||||
private var senderName: String? {
|
||||
annotation.kind.displayName ?? annotation.kind.userProfile?.userID
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, senderName: senderName)],
|
||||
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, senderName: senderName) })
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationCompactAdaptation(compactPresentation)
|
||||
.presentationDragIndicator(.hidden)
|
||||
}
|
||||
|
||||
private var compactPresentation: PresentationAdaptation {
|
||||
if #available(iOS 26.0, *) {
|
||||
.none // ShareLinks use a popover presentation on iOS 26, let it match that.
|
||||
} else {
|
||||
.sheet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct LocationShareSheet_Previews: PreviewProvider {
|
||||
static let profile = UserProfileProxy(userID: "@alice:example.com", displayName: "Alice")
|
||||
static let annotation = LocationAnnotation(id: profile.userID,
|
||||
coordinate: .init(latitude: 51.509865, longitude: -0.118092),
|
||||
kind: .liveUser(profile))
|
||||
|
||||
static var previews: some View {
|
||||
LocationShareSheet(annotation: annotation)
|
||||
.previewDisplayName("Live location share sheet")
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,12 @@ struct LocationSharingScreen: View {
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
StaticLocationSheet(context: context)
|
||||
.alert(item: $context.alertInfo)
|
||||
.popover(isPresented: $context.showShareSheet) { shareSheet }
|
||||
.popover(item: $context.sharedAnnotation) { annotation in
|
||||
LocationShareSheet(annotation: annotation)
|
||||
}
|
||||
}
|
||||
case .viewLive:
|
||||
mainContent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +39,7 @@ struct LocationSharingScreen: View {
|
||||
private var mainContent: some View {
|
||||
mapView
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
.track(screen: context.viewState.isLocationPickerMode ? .LocationSend : .LocationView)
|
||||
.track(screen: context.viewState.interactionMode == .picker ? .LocationSend : .LocationView)
|
||||
.navigationTitle(L10n.screenViewLocationTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
@@ -45,6 +49,7 @@ struct LocationSharingScreen: View {
|
||||
ZStack(alignment: .center) {
|
||||
MapLibreMapView(mapURLBuilder: context.viewState.mapURLBuilder,
|
||||
options: mapOptions,
|
||||
mediaProvider: context.mediaProvider,
|
||||
showsUserLocationMode: $context.showsUserLocationMode,
|
||||
error: $context.mapError,
|
||||
mapCenterCoordinate: $context.mapCenterLocation,
|
||||
@@ -53,10 +58,10 @@ struct LocationSharingScreen: View {
|
||||
geolocationUncertainty: $context.geolocationUncertainty) {
|
||||
context.send(viewAction: .userDidPan)
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: mapSafeAreaEdges)
|
||||
.ignoresSafeArea(edges: mapSafeAreaEdges)
|
||||
|
||||
if context.viewState.isLocationPickerMode {
|
||||
LocationMarkerView(kind: context.viewState.locationMarkerKind, mediaProvider: context.mediaProvider)
|
||||
if let pickerMarkerKind = context.viewState.pickerMarkerKind {
|
||||
LocationMarkerView(kind: pickerMarkerKind, mediaProvider: context.mediaProvider)
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
@@ -64,6 +69,10 @@ struct LocationSharingScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var mapSafeAreaEdges: Edge.Set {
|
||||
context.viewState.interactionMode == .picker ? .horizontal : [.horizontal, .bottom]
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
@@ -74,25 +83,10 @@ struct LocationSharingScreen: View {
|
||||
}
|
||||
|
||||
private var mapOptions: MapLibreMapView.Options {
|
||||
var annotations: [String: LocationAnnotation] = [:]
|
||||
if !context.viewState.isLocationPickerMode {
|
||||
let kind = context.viewState.locationMarkerKind
|
||||
let annotation = LocationAnnotation(id: kind.id,
|
||||
coordinate: context.viewState.initialMapCenter,
|
||||
anchorPoint: .bottomCenter) {
|
||||
LocationMarkerView(kind: kind, mediaProvider: context.mediaProvider)
|
||||
}
|
||||
annotations[kind.id] = annotation
|
||||
}
|
||||
|
||||
return .init(zoomLevel: context.viewState.zoomLevel,
|
||||
initialZoomLevel: context.viewState.initialZoomLevel,
|
||||
mapCenter: context.viewState.initialMapCenter,
|
||||
annotations: annotations)
|
||||
}
|
||||
|
||||
private var mapSafeAreaEdges: Edge.Set {
|
||||
context.viewState.isLocationPickerMode ? .horizontal : [.horizontal, .bottom]
|
||||
.init(zoomLevel: context.viewState.zoomLevel,
|
||||
initialZoomLevel: context.viewState.initialZoomLevel,
|
||||
mapCenter: context.viewState.initialMapCenter,
|
||||
annotations: context.viewState.annotations)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -125,26 +119,6 @@ struct LocationSharingScreen: View {
|
||||
.dynamicTypeSize(.large)
|
||||
.padding(13)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var shareSheet: some View {
|
||||
let location = context.viewState.initialMapCenter
|
||||
let senderName = context.viewState.locationMarkerKind.displayName ?? context.viewState.locationMarkerKind.userProfile?.userID
|
||||
AppActivityView(activityItems: [ShareToMapsAppActivity.MapsAppType.apple.activityURL(for: location, senderName: senderName)],
|
||||
applicationActivities: ShareToMapsAppActivity.MapsAppType.allCases.map { ShareToMapsAppActivity(type: $0, location: location, senderName: senderName) })
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationCompactAdaptation(shareSheetCompactPresentation)
|
||||
.presentationDragIndicator(.hidden)
|
||||
}
|
||||
|
||||
private var shareSheetCompactPresentation: PresentationAdaptation {
|
||||
if #available(iOS 26.0, *) {
|
||||
.none // ShareLinks use a popover presentation on iOS 26, let it match that.
|
||||
} else {
|
||||
.sheet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
@@ -180,7 +154,3 @@ struct LocationSharingScreen_Previews: PreviewProvider, TestablePreview {
|
||||
.previewDisplayName("Pin Static Location")
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGPoint {
|
||||
static let bottomCenter: Self = .init(x: 0.5, y: 1)
|
||||
}
|
||||
|
||||
@@ -34,12 +34,13 @@ struct StaticLocationSheet: View {
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.padding(.bottom, 25)
|
||||
.padding(.top, 29)
|
||||
if case let .viewStatic(location) = context.viewState.interactionMode {
|
||||
if case let .viewStatic(location) = context.viewState.interactionMode,
|
||||
let profile = context.viewState.userProfiles.values.first {
|
||||
Button {
|
||||
context.showShareSheet = true
|
||||
context.sharedAnnotation = context.viewState.annotations.first
|
||||
} label: {
|
||||
UserLocationCell(profile: context.viewState.userProfile,
|
||||
isOwnUser: context.viewState.isOwnUser,
|
||||
UserLocationCell(profile: profile,
|
||||
isOwnUser: context.viewState.isOwnUser(profile.userID),
|
||||
isUserLocation: location.kind == .sender,
|
||||
timestamp: location.timestamp,
|
||||
mediaProvider: context.mediaProvider)
|
||||
|
||||
+2
-2
@@ -72,7 +72,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
case .displayMediaDetails(item: let item):
|
||||
displayMediaPreviewSheet(for: item)
|
||||
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
|
||||
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
|
||||
.displayDocumentPicker, .displayLocationPicker, .displayLiveLocation, .displayPollForm, .displayMediaUploadPreviewScreen,
|
||||
.displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
|
||||
.displayThread, .composer, .hasScrolled, .viewInRoomTimeline, .displayRoom:
|
||||
break
|
||||
@@ -97,7 +97,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
case .displayMediaDetails(item: let item):
|
||||
displayMediaPreviewSheet(for: item)
|
||||
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
|
||||
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
|
||||
.displayDocumentPicker, .displayLocationPicker, .displayLiveLocation, .displayPollForm, .displayMediaUploadPreviewScreen,
|
||||
.displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
|
||||
.displayThread, .composer, .hasScrolled, .viewInRoomTimeline, .displayRoom:
|
||||
break
|
||||
|
||||
+3
@@ -27,6 +27,7 @@ enum PinnedEventsTimelineScreenCoordinatorAction {
|
||||
case dismiss
|
||||
case displayUser(userID: String)
|
||||
case presentLocationViewer(StaticLocationData)
|
||||
case presentLiveLocationViewer(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare)
|
||||
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
case displayRoomScreenWithFocussedPin(eventID: String, threadRootEventID: String?)
|
||||
}
|
||||
@@ -89,6 +90,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
viewModel.displayMediaPreview(mediaPreviewViewModel)
|
||||
case .displayLocation(let location):
|
||||
actionsSubject.send(.presentLocationViewer(location))
|
||||
case .displayLiveLocation(let sender, let initialLiveLocationShare):
|
||||
actionsSubject.send(.presentLiveLocationViewer(sender: sender, initialLiveLocationShare: initialLiveLocationShare))
|
||||
case .viewInRoomTimeline(let eventID, let threadRootEventID):
|
||||
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID, threadRootEventID: threadRootEventID))
|
||||
// These other actions will not be handled in this view
|
||||
|
||||
@@ -40,6 +40,7 @@ enum RoomScreenCoordinatorAction {
|
||||
case presentLocationPicker
|
||||
case presentPollForm(mode: PollFormMode)
|
||||
case presentLocationViewer(StaticLocationData)
|
||||
case presentLiveLocationViewer(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare)
|
||||
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
||||
case presentRoomMemberDetails(userID: String)
|
||||
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
@@ -140,6 +141,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
|
||||
case .displayLocation(let location):
|
||||
actionsSubject.send(.presentLocationViewer(location))
|
||||
case .displayLiveLocation(let sender, let initialLiveLocationShare):
|
||||
actionsSubject.send(.presentLiveLocationViewer(sender: sender, initialLiveLocationShare: initialLiveLocationShare))
|
||||
case .displayResolveSendFailure(let failure, let sendHandle):
|
||||
actionsSubject.send(.presentResolveSendFailure(failure: failure, sendHandle: sendHandle))
|
||||
case .displayThread(let itemID):
|
||||
|
||||
@@ -34,6 +34,7 @@ enum ThreadTimelineScreenCoordinatorAction {
|
||||
case presentMediaUploadPreviewScreen(mediaURLs: [URL])
|
||||
case presentLocationPicker
|
||||
case presentLocationViewer(StaticLocationData)
|
||||
case presentLiveLocationViewer(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare)
|
||||
case presentPollForm(mode: PollFormMode)
|
||||
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
|
||||
case presentRoomMemberDetails(userID: String)
|
||||
@@ -120,6 +121,8 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentLocationPicker)
|
||||
case .displayLocation(let location):
|
||||
actionsSubject.send(.presentLocationViewer(location))
|
||||
case .displayLiveLocation(let sender, let initialLiveLocationShare):
|
||||
actionsSubject.send(.presentLiveLocationViewer(sender: sender, initialLiveLocationShare: initialLiveLocationShare))
|
||||
case .displayPollForm(let mode):
|
||||
actionsSubject.send(.presentPollForm(mode: mode))
|
||||
case .displayMediaUploadPreviewScreen(let mediaURLs):
|
||||
|
||||
@@ -529,7 +529,7 @@ class TimelineInteractionHandler {
|
||||
}
|
||||
|
||||
func processItemTap(_ itemID: TimelineItemIdentifier) async -> TimelineControllerAction {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) as? EventBasedMessageTimelineItemProtocol else {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) as? EventBasedTimelineItemProtocol else {
|
||||
return .none
|
||||
}
|
||||
|
||||
@@ -540,12 +540,21 @@ class TimelineInteractionHandler {
|
||||
geoURI: geoURI,
|
||||
kind: item.content.kind,
|
||||
timestamp: item.timestamp))
|
||||
case is ImageRoomTimelineItem,
|
||||
is VideoRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: timelineItem, messageTypes: [.image, .video])
|
||||
case is AudioRoomTimelineItem,
|
||||
is FileRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: timelineItem, messageTypes: [.audio, .file])
|
||||
case let item as LiveLocationRoomTimelineItem:
|
||||
guard let geoURI = item.content.lastGeoURI else { return .none }
|
||||
let initialLiveLocationShare = LiveLocationShare(userID: item.sender.id,
|
||||
geoURI: geoURI,
|
||||
timestamp: item.timestamp,
|
||||
timeoutDate: item.content.timeoutDate)
|
||||
return .displayLiveLocation(sender: item.sender, initialLiveLocationShare: initialLiveLocationShare)
|
||||
case let item as ImageRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.image, .video])
|
||||
case let item as VideoRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.image, .video])
|
||||
case let item as AudioRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.audio, .file])
|
||||
case let item as FileRoomTimelineItem:
|
||||
return await mediaPreviewAction(for: item, messageTypes: [.audio, .file])
|
||||
default:
|
||||
return .none
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ enum TimelineViewModelAction {
|
||||
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
|
||||
case displayMediaPreview(TimelineMediaPreviewViewModel)
|
||||
case displayLocation(StaticLocationData)
|
||||
case displayLiveLocation(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare)
|
||||
case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
||||
case displayThread(itemID: TimelineItemIdentifier)
|
||||
case composer(action: TimelineComposerAction)
|
||||
|
||||
@@ -666,6 +666,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
actionsSubject.send(.displayMediaPreview(mediaPreviewViewModel))
|
||||
case .displayLocation(let location):
|
||||
actionsSubject.send(.displayLocation(location))
|
||||
case .displayLiveLocation(let sender, let initialLiveLocationShare):
|
||||
actionsSubject.send(.displayLiveLocation(sender: sender, initialLiveLocationShare: initialLiveLocationShare))
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user