Render LiveLocations in the map (#5394)

This commit is contained in:
Mauro
2026-04-14 18:27:34 +02:00
committed by GitHub
parent a40fab5f5e
commit 602d779e3a
19 changed files with 380 additions and 163 deletions
@@ -86,7 +86,10 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
case .displayUser(let userID):
actionsSubject.send(.displayUser(userID: userID))
case .presentLocationViewer(let location):
presentMapNavigator(location: location,
presentMapNavigator(interactionMode: .viewStatic(location),
timelineController: timelineController)
case .presentLiveLocationViewer(let sender, let initialLiveLocationShare):
presentMapNavigator(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialLiveLocationShare),
timelineController: timelineController)
case .displayMessageForwarding(let forwardingItem):
presentMessageForwarding(with: forwardingItem)
@@ -99,11 +102,11 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
navigationStackCoordinator.setRootCoordinator(coordinator)
}
private func presentMapNavigator(location: StaticLocationData,
private func presentMapNavigator(interactionMode: LocationSharingInteractionMode,
timelineController: TimelineControllerProtocol) {
let stackCoordinator = NavigationStackCoordinator()
let params = LocationSharingScreenCoordinatorParameters(interactionMode: .viewStatic(location),
let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode,
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration,
liveLocationSharingEnabled: flowParameters.appSettings.liveLocationSharingEnabled,
roomProxy: roomProxy,
@@ -712,6 +712,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewStatic(location)),
userInfo: EventUserInfo(animated: animated,
timelineController: timelineController))
case .presentLiveLocationViewer(let sender, let initialLiveLocationShare):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialLiveLocationShare)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentRoomMemberDetails(userID: let userID):
stateMachine.tryEvent(.startMembersFlow(entryPoint: .roomMember(userID: userID)))
case .presentMessageForwarding(let forwardingItem):
@@ -811,6 +814,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .presentLocationPicker:
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLiveLocationViewer(let sender, let initialLiveLocationShare):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialLiveLocationShare)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentPollForm(let mode):
stateMachine.tryEvent(.presentPollForm(mode: mode),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
@@ -6,32 +6,24 @@
// Please see LICENSE files in the repository root for full details.
//
import CoreLocation
import Foundation
import MapLibre
import SwiftUI
final class LocationAnnotation: NSObject, MLNAnnotation {
final class LocationAnnotation: NSObject, MLNAnnotation, Identifiable {
let id: String
var coordinate: CLLocationCoordinate2D
let anchorPoint: CGPoint
var view: AnyView
var kind: LocationMarkerKind
// MARK: - Setup
init(id: String = UUID().uuidString,
coordinate: CLLocationCoordinate2D,
anchorPoint: CGPoint = .init(x: 0.5, y: 0.5),
@ViewBuilder label: () -> some View) {
init(id: String, coordinate: CLLocationCoordinate2D, kind: LocationMarkerKind) {
self.id = id
self.coordinate = coordinate
self.anchorPoint = anchorPoint
view = AnyView(label())
self.kind = kind
super.init()
}
func updateView(@ViewBuilder label: () -> some View) {
view = AnyView(label())
}
}
final class LocationAnnotationView: MLNUserLocationAnnotationView {
@@ -44,19 +36,21 @@ final class LocationAnnotationView: MLNUserLocationAnnotationView {
reuseIdentifier)
}
convenience init(annotation: LocationAnnotation) {
convenience init(annotation: LocationAnnotation, mediaProvider: MediaProviderProtocol?) {
self.init(annotation: annotation, reuseIdentifier: "\(Self.self)")
let hostingController = UIHostingController(rootView: annotation.view)
let markerView = LocationMarkerView(kind: annotation.kind, mediaProvider: mediaProvider)
let hostingController = UIHostingController(rootView: AnyView(markerView))
self.hostingController = hostingController
let view: UIView = hostingController.view
view.backgroundColor = .clear
view.anchorPoint = annotation.anchorPoint
view.anchorPoint = .init(x: 0.5, y: 1.0)
addSubview(view)
view.bounds.size = view.intrinsicContentSize
}
func updateContent(with view: AnyView) {
hostingController?.rootView = view
func updateContent(with kind: LocationMarkerKind, mediaProvider: MediaProviderProtocol?) {
let markerView = LocationMarkerView(kind: kind, mediaProvider: mediaProvider)
hostingController?.rootView = AnyView(markerView)
if let hostedView = hostingController?.view {
hostedView.bounds.size = hostedView.intrinsicContentSize
}
@@ -20,10 +20,10 @@ struct MapLibreMapView: UIViewRepresentable {
/// The initial map center
let mapCenter: CLLocationCoordinate2D
/// Map annotations keyed by a stable identifier (e.g. sender userID for user pins, UUID string for generic pins)
let annotations: [String: LocationAnnotation]
/// Map annotations
let annotations: [LocationAnnotation]
init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [String: LocationAnnotation] = [:]) {
init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [LocationAnnotation] = []) {
self.zoomLevel = zoomLevel
self.initialZoomLevel = initialZoomLevel
self.mapCenter = mapCenter
@@ -39,6 +39,8 @@ struct MapLibreMapView: UIViewRepresentable {
let options: Options
let mediaProvider: MediaProviderProtocol?
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
@Binding var showsUserLocationMode: ShowUserLocationMode
/// Bind view errors if any
@@ -86,7 +88,7 @@ struct MapLibreMapView: UIViewRepresentable {
// MARK: - Private
private func setupMap(mapView: MLNMapView, with options: Options) {
mapView.addAnnotations(Array(options.annotations.values))
mapView.addAnnotations(options.annotations)
mapView.zoomLevel = options.annotations.isEmpty ? options.initialZoomLevel : options.zoomLevel
mapView.centerCoordinate = options.mapCenter
}
@@ -94,9 +96,10 @@ struct MapLibreMapView: UIViewRepresentable {
private func updateAnnotations(in mapView: MLNMapView) {
let existingByID = Dictionary(uniqueKeysWithValues:
(mapView.annotations ?? []).compactMap { $0 as? LocationAnnotation }.map { ($0.id, $0) })
let updatedByID = Dictionary(uniqueKeysWithValues: options.annotations.map { ($0.id, $0) })
let existingIDs = Set(existingByID.keys)
let updatedIDs = Set(options.annotations.keys)
let updatedIDs = Set(updatedByID.keys)
// Remove annotations that are no longer present
let removedIDs = existingIDs.subtracting(updatedIDs)
@@ -108,7 +111,7 @@ struct MapLibreMapView: UIViewRepresentable {
// Add new annotations
let addedIDs = updatedIDs.subtracting(existingIDs)
if !addedIDs.isEmpty {
let toAdd = addedIDs.compactMap { options.annotations[$0] }
let toAdd = addedIDs.compactMap { updatedByID[$0] }
mapView.addAnnotations(toAdd)
}
@@ -116,12 +119,12 @@ struct MapLibreMapView: UIViewRepresentable {
let keptIDs = existingIDs.intersection(updatedIDs)
for id in keptIDs {
guard let existingAnnotation = existingByID[id],
let updatedAnnotation = options.annotations[id] else {
let updatedAnnotation = updatedByID[id] else {
continue
}
existingAnnotation.coordinate = updatedAnnotation.coordinate
if let annotationView = mapView.view(for: existingAnnotation) as? LocationAnnotationView {
annotationView.updateContent(with: updatedAnnotation.view)
annotationView.updateContent(with: updatedAnnotation.kind, mediaProvider: mediaProvider)
}
}
}
@@ -179,7 +182,7 @@ extension MapLibreMapView {
guard let annotation = annotation as? LocationAnnotation else {
return nil
}
return LocationAnnotationView(annotation: annotation)
return LocationAnnotationView(annotation: annotation, mediaProvider: mapLibreView.mediaProvider)
}
func mapViewDidFailLoadingMap(_ mapView: MLNMapView, withError error: Error) {
@@ -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)
@@ -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
@@ -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
}
@@ -26,6 +26,7 @@ enum TimelineControllerAction {
case displayMediaPreview(item: EventBasedMessageTimelineItemProtocol, timelineViewModel: TimelineViewModelKind)
case displayLocation(StaticLocationData)
case displayLiveLocation(sender: TimelineItemSender, initialLiveLocationShare: LiveLocationShare)
case none
}