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), stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLocationPicker: case .presentLocationPicker:
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker), stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker(shouldShowLiveLocationOption: true)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentPollForm(let mode): case .presentPollForm(let mode):
stateMachine.tryEvent(.presentPollForm(mode: mode), stateMachine.tryEvent(.presentPollForm(mode: mode),
@@ -814,7 +814,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaURLs), stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaURLs),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLocationPicker: case .presentLocationPicker:
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker), stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker(shouldShowLiveLocationOption: false)),
userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) userInfo: EventUserInfo(animated: animated, timelineController: timelineController))
case .presentLiveLocationViewer(let sender, let initialLiveLocationShare): case .presentLiveLocationViewer(let sender, let initialLiveLocationShare):
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialLiveLocationShare)), stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialLiveLocationShare)),
@@ -1167,7 +1167,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
timelineController: TimelineControllerProtocol, timelineController: TimelineControllerProtocol,
animated: Bool) { animated: Bool) {
let stackCoordinator = NavigationStackCoordinator() let stackCoordinator = NavigationStackCoordinator()
let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode, let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode,
mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration, mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration,
roomProxy: roomProxy, roomProxy: roomProxy,
@@ -224,7 +224,7 @@ extension RoomFlowCoordinator {
case (.mediaEventsTimeline, .presentMessageForwarding(forwardingItem: let forwardingItem)): case (.mediaEventsTimeline, .presentMessageForwarding(forwardingItem: let forwardingItem)):
return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState) return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState)
case (.room, .presentMapNavigator(_)): case (.room, .presentMapNavigator):
return .mapNavigator(previousState: fromState) return .mapNavigator(previousState: fromState)
case (.room, .presentPollForm): case (.room, .presentPollForm):
@@ -271,7 +271,7 @@ extension RoomFlowCoordinator {
case (.thread, .presentMessageForwarding(let forwardingItem)): case (.thread, .presentMessageForwarding(let forwardingItem)):
return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState) return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState)
case (.thread, .presentMapNavigator(_)): case (.thread, .presentMapNavigator):
return .mapNavigator(previousState: fromState) return .mapNavigator(previousState: fromState)
case (.thread, .presentPollForm): case (.thread, .presentPollForm):
@@ -25,9 +25,21 @@ enum LocationSharingScreenViewModelAction {
} }
enum LocationSharingInteractionMode: Hashable { enum LocationSharingInteractionMode: Hashable {
case picker case picker(shouldShowLiveLocationOption: Bool)
case viewStatic(StaticLocationData) case viewStatic(StaticLocationData)
case viewLive(sender: TimelineItemSender?, initialLiveLocationShare: LiveLocationShare?) 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 { struct LocationSharingScreenViewState: BindableState {
@@ -46,7 +46,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
self.analytics = analytics self.analytics = analytics
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.notificationCenter = notificationCenter self.notificationCenter = notificationCenter
super.init(initialViewState: .init(interactionMode: interactionMode, super.init(initialViewState: .init(interactionMode: interactionMode,
mapURLBuilder: mapURLBuilder, mapURLBuilder: mapURLBuilder,
ownUserID: roomProxy.ownUserID), ownUserID: roomProxy.ownUserID),
@@ -332,17 +332,20 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati
extension LocationSharingScreenViewModel { extension LocationSharingScreenViewModel {
enum MockType { enum MockType {
case picker case picker
case pickerWithoutLiveLocationOption
case staticSenderLocation case staticSenderLocation
case staticPinLocation case staticPinLocation
case viewLive case viewLive
case viewLiveEmpty case viewLiveEmpty
} }
static func mock(type: MockType, static func mock(type: MockType,
senderID: String = "@dan:matrix.org") -> LocationSharingScreenViewModel { senderID: String = "@dan:matrix.org") -> LocationSharingScreenViewModel {
let interactionMode: LocationSharingInteractionMode = switch type { let interactionMode: LocationSharingInteractionMode = switch type {
case .picker: case .picker:
.picker .picker(shouldShowLiveLocationOption: true)
case .pickerWithoutLiveLocationOption:
.picker(shouldShowLiveLocationOption: false)
case .staticPinLocation: case .staticPinLocation:
.viewStatic(.init(sender: .init(id: senderID), .viewStatic(.init(sender: .init(id: senderID),
geoURI: .init(latitude: 41.9027835, geoURI: .init(latitude: 41.9027835,
@@ -11,7 +11,14 @@ import SwiftUI
struct LocationPickerSheet: View { struct LocationPickerSheet: View {
@Bindable var context: LocationSharingScreenViewModel.Context @Bindable var context: LocationSharingScreenViewModel.Context
@State private var height: CGFloat = .zero @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 { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
Text(L10n.screenSharingLocationOptionSheetTitle) Text(L10n.screenSharingLocationOptionSheetTitle)
@@ -34,12 +41,14 @@ struct LocationPickerSheet: View {
} }
} }
Button { if context.viewState.interactionMode.shouldShowLiveLocationOption {
context.send(viewAction: .startLiveLocation) Button {
} label: { context.send(viewAction: .startLiveLocation)
LocationPickerLabel(text: L10n.actionShareLiveLocation, } label: {
icon: \.locationPinSolid, LocationPickerLabel(text: L10n.actionShareLiveLocation,
iconColor: .compound.iconAccentPrimary) icon: \.locationPinSolid,
iconColor: .compound.iconAccentPrimary)
}
} }
} }
.readHeight($height) .readHeight($height)
@@ -47,7 +56,7 @@ struct LocationPickerSheet: View {
.presentationBackground(.compound.bgCanvasDefault) .presentationBackground(.compound.bgCanvasDefault)
.presentationBackgroundInteraction(.enabled) .presentationBackgroundInteraction(.enabled)
.presentationDragIndicator(.hidden) .presentationDragIndicator(.hidden)
.presentationDetents([.height(height)]) .presentationDetents([.height(height + additionalHeight)])
} }
} }
@@ -40,7 +40,7 @@ struct LocationSharingScreen: View {
private var mainContent: some View { private var mainContent: some View {
mapView mapView
.ignoresSafeArea(edges: .bottom) .ignoresSafeArea(edges: .bottom)
.track(screen: context.viewState.interactionMode == .picker ? .LocationSend : .LocationView) .track(screen: context.viewState.interactionMode.isPicker ? .LocationSend : .LocationView)
.navigationTitle(L10n.screenViewLocationTitle) .navigationTitle(L10n.screenViewLocationTitle)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar } .toolbar { toolbar }
@@ -71,7 +71,7 @@ struct LocationSharingScreen: View {
} }
private var mapSafeAreaEdges: Edge.Set { private var mapSafeAreaEdges: Edge.Set {
context.viewState.interactionMode == .picker ? .horizontal : [.horizontal, .bottom] context.viewState.interactionMode.isPicker ? .horizontal : [.horizontal, .bottom]
} }
@ToolbarContentBuilder @ToolbarContentBuilder
@@ -126,19 +126,26 @@ struct LocationSharingScreen: View {
struct LocationSharingScreen_Previews: PreviewProvider, TestablePreview { struct LocationSharingScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = LocationSharingScreenViewModel.mock(type: .staticSenderLocation) static let viewModel = LocationSharingScreenViewModel.mock(type: .staticSenderLocation)
static let withoutLiveSharingViewModel = LocationSharingScreenViewModel.mock(type: .pickerWithoutLiveLocationOption)
static let pinViewModel = LocationSharingScreenViewModel.mock(type: .staticPinLocation) static let pinViewModel = LocationSharingScreenViewModel.mock(type: .staticPinLocation)
static let pickerViewModel = LocationSharingScreenViewModel.mock(type: .picker) static let pickerViewModel = LocationSharingScreenViewModel.mock(type: .picker)
static let liveLocationViewModel = LocationSharingScreenViewModel.mock(type: .viewLive) static let liveLocationViewModel = LocationSharingScreenViewModel.mock(type: .viewLive)
static var previews: some View { static var previews: some View {
ElementNavigationStack { ElementNavigationStack {
LocationSharingScreen(context: pickerViewModel.context) LocationSharingScreen(context: pickerViewModel.context)
} }
.previewDisplayName("Picker") .previewDisplayName("Picker")
ElementNavigationStack {
LocationSharingScreen(context: withoutLiveSharingViewModel.context)
}
.previewDisplayName("Picker without live location sharing")
ElementNavigationStack { ElementNavigationStack {
LocationSharingScreen(context: viewModel.context) LocationSharingScreen(context: viewModel.context)
} }
@@ -440,7 +440,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
timestamp: eventItemProxy.timestamp, timestamp: eventItemProxy.timestamp,
isOutgoing: isOutgoing, isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable, 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, sender: eventItemProxy.sender,
content: .init(from: liveLocationContent, timestamp: eventItemProxy.timestamp), content: .init(from: liveLocationContent, timestamp: eventItemProxy.timestamp),
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo), 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, analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MediaProviderMock(configuration: .init())) mediaProvider: MediaProviderMock(configuration: .init()))
// Initially no annotations and no map center since sender and share are both nil. // Initially no annotations and no map center since sender and share are both nil.
#expect(context.viewState.annotations.isEmpty) #expect(context.viewState.annotations.isEmpty)
#expect(context.mapCenterLocation == nil) #expect(context.mapCenterLocation == nil)
@@ -428,7 +428,7 @@ struct LocationSharingScreenViewModelTests {
private mutating func setupViewModel(liveLocationManagerConfiguration: LiveLocationManagerMock.Configuration = .init(), private mutating func setupViewModel(liveLocationManagerConfiguration: LiveLocationManagerMock.Configuration = .init(),
members: [RoomMemberProxyMock] = .allMembersAsAdmin) { members: [RoomMemberProxyMock] = .allMembersAsAdmin) {
timelineProxy = TimelineProxyMock(.init()) timelineProxy = TimelineProxyMock(.init())
viewModel = LocationSharingScreenViewModel(interactionMode: .picker, viewModel = LocationSharingScreenViewModel(interactionMode: .picker(shouldShowLiveLocationOption: true),
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration, mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
roomProxy: JoinedRoomProxyMock(.init(members: members)), roomProxy: JoinedRoomProxyMock(.init(members: members)),
timelineController: MockTimelineController(timelineProxy: timelineProxy), timelineController: MockTimelineController(timelineProxy: timelineProxy),
@@ -438,11 +438,11 @@ struct LocationSharingScreenViewModelTests {
mediaProvider: MediaProviderMock(configuration: .init())) mediaProvider: MediaProviderMock(configuration: .init()))
viewModel.state.bindings.isLocationAuthorized = true viewModel.state.bindings.isLocationAuthorized = true
} }
private mutating func setupViewModel(liveLocationManagerMock: LiveLocationManagerMock, private mutating func setupViewModel(liveLocationManagerMock: LiveLocationManagerMock,
members: [RoomMemberProxyMock] = .allMembersAsAdmin) { members: [RoomMemberProxyMock] = .allMembersAsAdmin) {
timelineProxy = TimelineProxyMock(.init()) timelineProxy = TimelineProxyMock(.init())
viewModel = LocationSharingScreenViewModel(interactionMode: .picker, viewModel = LocationSharingScreenViewModel(interactionMode: .picker(shouldShowLiveLocationOption: true),
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration, mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
roomProxy: JoinedRoomProxyMock(.init(members: members)), roomProxy: JoinedRoomProxyMock(.init(members: members)),
timelineController: MockTimelineController(timelineProxy: timelineProxy), timelineController: MockTimelineController(timelineProxy: timelineProxy),