Render LiveLocations in the map (#5394)

This commit is contained in:
Mauro
2026-04-14 18:27:34 +02:00
committed by GitHub
parent c9cafe0106
commit a27effdac6
19 changed files with 380 additions and 163 deletions

View File

@@ -257,6 +257,72 @@ final class LocationSharingScreenViewModelTests {
try await deferredFailure.fulfill()
}
// MARK: - Live Location Share Update Tests
@Test
func viewLiveInitialSenderShownCorrectly() {
let aliceShare = makeLiveLocationShare(userID: "@alice:matrix.org", latitude: 51.5, longitude: -0.1)
let sender = TimelineItemSender(id: "@alice:matrix.org", displayName: "Alice")
let liveLocationsSubject = CurrentValueSubject<[LiveLocationShare], Never>([aliceShare])
setupViewModelForViewLive(sender: sender, initialShare: aliceShare, liveLocationsSubject: liveLocationsSubject)
// Initial state is synchronously set from the interaction mode before the async subscription runs.
let annotations = context.viewState.annotations
#expect(annotations.count == 1)
let annotation = annotations.first
#expect(annotation?.id == "@alice:matrix.org")
#expect(annotation?.coordinate.latitude == 51.5)
#expect(annotation?.coordinate.longitude == -0.1)
#expect(annotation?.kind == .liveUser(.init(userID: "@alice:matrix.org", displayName: "Alice")))
}
@Test
func viewLiveReceivesAdditionalLocationUpdates() async throws {
let aliceShare = makeLiveLocationShare(userID: "@alice:matrix.org", latitude: 51.5, longitude: -0.1)
let sender = TimelineItemSender(id: "@alice:matrix.org", displayName: "Alice")
let liveLocationsSubject = CurrentValueSubject<[LiveLocationShare], Never>([aliceShare])
setupViewModelForViewLive(sender: sender, initialShare: aliceShare, liveLocationsSubject: liveLocationsSubject)
let bobShare = makeLiveLocationShare(userID: "@bob:matrix.org", latitude: 48.8, longitude: 2.3)
let charlieShare = makeLiveLocationShare(userID: "@charlie:matrix.org", latitude: 40.7, longitude: -74.0)
let deferred = deferFulfillment(context.observe(\.viewState.annotations)) { $0.count == 3 }
liveLocationsSubject.send([aliceShare, bobShare, charlieShare])
try await deferred.fulfill()
let annotations = context.viewState.annotations
#expect(annotations.count == 3)
let annotationIDs = Set(annotations.map(\.id))
#expect(annotationIDs == ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"])
#expect(annotations.first { $0.id == "@alice:matrix.org" }?.coordinate.latitude == 51.5)
#expect(annotations.first { $0.id == "@bob:matrix.org" }?.coordinate.latitude == 48.8)
#expect(annotations.first { $0.id == "@charlie:matrix.org" }?.coordinate.latitude == 40.7)
}
@Test
func viewLiveProfilesResolvedFromRoomMembers() async throws {
let aliceShare = makeLiveLocationShare(userID: "@alice:matrix.org", latitude: 51.5, longitude: -0.1)
let sender = TimelineItemSender(id: "@alice:matrix.org", displayName: "Alice")
let liveLocationsSubject = CurrentValueSubject<[LiveLocationShare], Never>([aliceShare])
setupViewModelForViewLive(sender: sender, initialShare: aliceShare, liveLocationsSubject: liveLocationsSubject)
let bobShare = makeLiveLocationShare(userID: "@bob:matrix.org", latitude: 48.8, longitude: 2.3)
let charlieShare = makeLiveLocationShare(userID: "@charlie:matrix.org", latitude: 40.7, longitude: -74.0)
let deferred = deferFulfillment(context.observe(\.viewState.annotations)) { $0.count == 3 }
liveLocationsSubject.send([aliceShare, bobShare, charlieShare])
try await deferred.fulfill()
// Annotation marker kinds should carry profiles resolved from room members.
let annotations = context.viewState.annotations
#expect(annotations.first { $0.id == "@alice:matrix.org" }?.kind == .liveUser(.init(userID: "@alice:matrix.org", displayName: "Alice")))
#expect(annotations.first { $0.id == "@bob:matrix.org" }?.kind == .liveUser(.init(userID: "@bob:matrix.org", displayName: "Bob")))
#expect(annotations.first { $0.id == "@charlie:matrix.org" }?.kind == .liveUser(.init(userID: "@charlie:matrix.org", displayName: "Charlie")))
}
// MARK: - Private
private func setupViewModel(liveLocationManagerConfiguration: LiveLocationManagerMock.Configuration = .init()) {
@@ -286,4 +352,32 @@ final class LocationSharingScreenViewModelTests {
mediaProvider: MediaProviderMock(configuration: .init()))
viewModel.state.bindings.isLocationAuthorized = true
}
private func setupViewModelForViewLive(sender: TimelineItemSender,
initialShare: LiveLocationShare,
liveLocationsSubject: CurrentValueSubject<[LiveLocationShare], Never>,
members: [RoomMemberProxyMock] = .allMembers) {
let liveLocationServiceMock = RoomLiveLocationServiceMock()
liveLocationServiceMock.liveLocationsPublisher = liveLocationsSubject.eraseToAnyPublisher()
let roomProxyMock = JoinedRoomProxyMock(.init(members: members))
roomProxyMock.makeLiveLocationServiceReturnValue = liveLocationServiceMock
viewModel = LocationSharingScreenViewModel(interactionMode: .viewLive(sender: sender, initialLiveLocationShare: initialShare),
mapURLBuilder: ServiceLocator.shared.settings.mapTilerConfiguration,
liveLocationSharingEnabled: true,
roomProxy: roomProxyMock,
timelineController: MockTimelineController(timelineProxy: TimelineProxyMock(.init())),
liveLocationManager: LiveLocationManagerMock(.init()),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MediaProviderMock(configuration: .init()))
}
private func makeLiveLocationShare(userID: String, latitude: Double = 0.0, longitude: Double = 0.0) -> LiveLocationShare {
LiveLocationShare(userID: userID,
geoURI: .init(latitude: latitude, longitude: longitude),
timestamp: .distantPast,
timeoutDate: .distantFuture)
}
}