prevent replies to LLS timeline items, and live location sharing in threads

This commit is contained in:
Mauro Romito
2026-05-12 14:42:12 +02:00
committed by Mauro
parent 212ac17d0b
commit 07fc022a65
12 changed files with 73 additions and 29 deletions
@@ -705,7 +705,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLocationPicker:
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker),
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker(shouldShowLiveLocationOption: true)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentPollForm(let mode):
stateMachine.tryEvent(.presentPollForm(mode: mode),
@@ -814,7 +814,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaURLs),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLocationPicker:
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker),
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker(shouldShowLiveLocationOption: false)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLiveLocationViewer(let sender, let initialLiveLocationShare):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialLiveLocationShare)),
@@ -1167,7 +1167,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
timelineController: TimelineControllerProtocol,
animated: Bool) {
let stackCoordinator = NavigationStackCoordinator()
let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode,
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration,
roomProxy: roomProxy,
@@ -224,7 +224,7 @@ extension RoomFlowCoordinator {
case (.mediaEventsTimeline, .presentMessageForwarding(forwardingItem: let forwardingItem)):
return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState)
case (.room, .presentMapNavigator(_)):
case (.room, .presentMapNavigator):
return .mapNavigator(previousState: fromState)
case (.room, .presentPollForm):
@@ -271,7 +271,7 @@ extension RoomFlowCoordinator {
case (.thread, .presentMessageForwarding(let forwardingItem)):
return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState)
case (.thread, .presentMapNavigator(_)):
case (.thread, .presentMapNavigator):
return .mapNavigator(previousState: fromState)
case (.thread, .presentPollForm):
@@ -25,9 +25,21 @@ enum LocationSharingScreenViewModelAction {
}
enum LocationSharingInteractionMode: Hashable {
case picker
case picker(shouldShowLiveLocationOption: Bool)
case viewStatic(StaticLocationData)
case viewLive(sender: TimelineItemSender?, initialLiveLocationShare: LiveLocationShare?)
var isPicker: Bool {
if case .picker = self { return true }
return false
}
var shouldShowLiveLocationOption: Bool {
if case .picker(let shouldShowLiveLocationOption) = self {
return shouldShowLiveLocationOption
}
return false
}
}
struct LocationSharingScreenViewState: BindableState {
@@ -46,7 +46,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
self.analytics = analytics
self.userIndicatorController = userIndicatorController
self.notificationCenter = notificationCenter
super.init(initialViewState: .init(interactionMode: interactionMode,
mapURLBuilder: mapURLBuilder,
ownUserID: roomProxy.ownUserID),
@@ -332,17 +332,20 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
extension LocationSharingScreenViewModel {
enum MockType {
case picker
case pickerWithoutLiveLocationOption
case staticSenderLocation
case staticPinLocation
case viewLive
case viewLiveEmpty
}
static func mock(type: MockType,
senderID: String = "@dan:matrix.org") -> LocationSharingScreenViewModel {
let interactionMode: LocationSharingInteractionMode = switch type {
case .picker:
.picker
.picker(shouldShowLiveLocationOption: true)
case .pickerWithoutLiveLocationOption:
.picker(shouldShowLiveLocationOption: false)
case .staticPinLocation:
.viewStatic(.init(sender: .init(id: senderID),
geoURI: .init(latitude: 41.9027835,
@@ -11,7 +11,14 @@ import SwiftUI
struct LocationPickerSheet: View {
@Bindable var context: LocationSharingScreenViewModel.Context
@State private var height: CGFloat = .zero
/// Fixes an iOS 26 sheet issue
/// if the content doesn't meet a certain size
/// additional insets are added.
private var additionalHeight: CGFloat {
context.viewState.interactionMode.shouldShowLiveLocationOption ? 0 : 28
}
var body: some View {
VStack(spacing: 0) {
Text(L10n.screenSharingLocationOptionSheetTitle)
@@ -34,12 +41,14 @@ struct LocationPickerSheet: View {
}
}
Button {
context.send(viewAction: .startLiveLocation)
} label: {
LocationPickerLabel(text: L10n.actionShareLiveLocation,
icon: \.locationPinSolid,
iconColor: .compound.iconAccentPrimary)
if context.viewState.interactionMode.shouldShowLiveLocationOption {
Button {
context.send(viewAction: .startLiveLocation)
} label: {
LocationPickerLabel(text: L10n.actionShareLiveLocation,
icon: \.locationPinSolid,
iconColor: .compound.iconAccentPrimary)
}
}
}
.readHeight($height)
@@ -47,7 +56,7 @@ struct LocationPickerSheet: View {
.presentationBackground(.compound.bgCanvasDefault)
.presentationBackgroundInteraction(.enabled)
.presentationDragIndicator(.hidden)
.presentationDetents([.height(height)])
.presentationDetents([.height(height + additionalHeight)])
}
}
@@ -40,7 +40,7 @@ struct LocationSharingScreen: View {
private var mainContent: some View {
mapView
.ignoresSafeArea(edges: .bottom)
.track(screen: context.viewState.interactionMode == .picker ? .LocationSend : .LocationView)
.track(screen: context.viewState.interactionMode.isPicker ? .LocationSend : .LocationView)
.navigationTitle(L10n.screenViewLocationTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
@@ -71,7 +71,7 @@ struct LocationSharingScreen: View {
}
private var mapSafeAreaEdges: Edge.Set {
context.viewState.interactionMode == .picker ? .horizontal : [.horizontal, .bottom]
context.viewState.interactionMode.isPicker ? .horizontal : [.horizontal, .bottom]
}
@ToolbarContentBuilder
@@ -126,19 +126,26 @@ struct LocationSharingScreen: View {
struct LocationSharingScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = LocationSharingScreenViewModel.mock(type: .staticSenderLocation)
static let withoutLiveSharingViewModel = LocationSharingScreenViewModel.mock(type: .pickerWithoutLiveLocationOption)
static let pinViewModel = LocationSharingScreenViewModel.mock(type: .staticPinLocation)
static let pickerViewModel = LocationSharingScreenViewModel.mock(type: .picker)
static let liveLocationViewModel = LocationSharingScreenViewModel.mock(type: .viewLive)
static var previews: some View {
ElementNavigationStack {
LocationSharingScreen(context: pickerViewModel.context)
}
.previewDisplayName("Picker")
ElementNavigationStack {
LocationSharingScreen(context: withoutLiveSharingViewModel.context)
}
.previewDisplayName("Picker without live location sharing")
ElementNavigationStack {
LocationSharingScreen(context: viewModel.context)
}
@@ -440,7 +440,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
// Workaround until https://github.com/matrix-org/matrix-rust-sdk/pull/6563 is merged.
canBeRepliedTo: false,
sender: eventItemProxy.sender,
content: .init(from: liveLocationContent, timestamp: eventItemProxy.timestamp),
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64737509400fe27cdb017b8f66cffe8c0807f958827cff84a9ce5484db0ca3fc
size 80109
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64737509400fe27cdb017b8f66cffe8c0807f958827cff84a9ce5484db0ca3fc
size 80109
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08e72ae0c7d34108690371b29952267b1ac06bb62f203515f3c5e4143efd2ca8
size 38511
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08e72ae0c7d34108690371b29952267b1ac06bb62f203515f3c5e4143efd2ca8
size 38511
@@ -402,7 +402,7 @@ struct LocationSharingScreenViewModelTests {
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MediaProviderMock(configuration: .init()))
// Initially no annotations and no map center since sender and share are both nil.
#expect(context.viewState.annotations.isEmpty)
#expect(context.mapCenterLocation == nil)
@@ -428,7 +428,7 @@ struct LocationSharingScreenViewModelTests {
private mutating func setupViewModel(liveLocationManagerConfiguration: LiveLocationManagerMock.Configuration = .init(),
members: [RoomMemberProxyMock] = .allMembersAsAdmin) {
timelineProxy = TimelineProxyMock(.init())
viewModel = LocationSharingScreenViewModel(interactionMode: .picker,
viewModel = LocationSharingScreenViewModel(interactionMode: .picker(shouldShowLiveLocationOption: true),
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
roomProxy: JoinedRoomProxyMock(.init(members: members)),
timelineController: MockTimelineController(timelineProxy: timelineProxy),
@@ -438,11 +438,11 @@ struct LocationSharingScreenViewModelTests {
mediaProvider: MediaProviderMock(configuration: .init()))
viewModel.state.bindings.isLocationAuthorized = true
}
private mutating func setupViewModel(liveLocationManagerMock: LiveLocationManagerMock,
members: [RoomMemberProxyMock] = .allMembersAsAdmin) {
timelineProxy = TimelineProxyMock(.init())
viewModel = LocationSharingScreenViewModel(interactionMode: .picker,
viewModel = LocationSharingScreenViewModel(interactionMode: .picker(shouldShowLiveLocationOption: true),
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
roomProxy: JoinedRoomProxyMock(.init(members: members)),
timelineController: MockTimelineController(timelineProxy: timelineProxy),