mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Live Location Sharing Banner (#5370)
* Live Location Sharing Banner # Conflicts: # ElementX.xcodeproj/project.pbxproj * updated the top banner modifier and the top banner views # Conflicts: # ElementX/Resources/Localizations/en-US.lproj/Localizable.strings # ElementX/Resources/Localizations/en.lproj/Localizable.strings * improved the stopping function from the timeline item * stop live location sharing before starting a new one. * added some tests for LiveLocationManager * pr suggestions
This commit is contained in:
@@ -303,6 +303,10 @@ extension AccessibilityTests {
|
||||
try await performAccessibilityAudit(named: "LiveLocationRoomTimelineView_Previews")
|
||||
}
|
||||
|
||||
func testLiveLocationSharingBannerView() async throws {
|
||||
try await performAccessibilityAudit(named: "LiveLocationSharingBannerView_Previews")
|
||||
}
|
||||
|
||||
func testLoadableImage() async throws {
|
||||
try await performAccessibilityAudit(named: "LoadableImage_Previews")
|
||||
}
|
||||
|
||||
@@ -860,6 +860,7 @@
|
||||
92012C96039BC8C2CAEBA9E2 /* AuthenticationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671C338B7259DC5774816885 /* AuthenticationServiceTests.swift */; };
|
||||
920DC020F18ABC88175114D3 /* SpaceListScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5557DDA438841AF5DC003D0B /* SpaceListScreenViewModelTests.swift */; };
|
||||
9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; };
|
||||
9223E5F2A2CE0AFFDFF0AFFB /* LiveLocationSharingBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C60559596897AC65D2CB799 /* LiveLocationSharingBannerView.swift */; };
|
||||
92720AB0DA9AB5EEF1DAF56B /* SecureBackupLogoutConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */; };
|
||||
9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */; };
|
||||
92919B6522B26B2681232EAC /* LiveLocationManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5DF0E888F66652F8C4CEC5 /* LiveLocationManagerMock.swift */; };
|
||||
@@ -1191,6 +1192,7 @@
|
||||
CB498F4E27AA0545DCEF0F6F /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; };
|
||||
CB6956565D858C523E3E3B16 /* ComposerDraftServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */; };
|
||||
CB6BCBF28E4B76EA08C2926D /* StateRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B16048D30F0438731C41F775 /* StateRoomTimelineItem.swift */; };
|
||||
CB6FBAF32B1305F914CF4225 /* LiveLocationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5CB64E711E86270B722FAF7 /* LiveLocationManagerTests.swift */; };
|
||||
CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; };
|
||||
CB9FB2BEF313072C705AC9B5 /* SecurityAndPrivacyScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315C328FF40F84276364E66 /* SecurityAndPrivacyScreenViewModelTests.swift */; };
|
||||
CBBBE597BE74A2DF68DE2209 /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDB7A9BB466C56614BB589D /* NotificationItemProxyProtocol.swift */; };
|
||||
@@ -2107,6 +2109,7 @@
|
||||
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
5C1F000589F2CEE6B03ECFAB /* TimelineMediaPreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewViewModelTests.swift; sourceTree = "<group>"; };
|
||||
5C60559596897AC65D2CB799 /* LiveLocationSharingBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationSharingBannerView.swift; sourceTree = "<group>"; };
|
||||
5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
5CABD320DE5566D133890B24 /* AccessibilityTestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityTestsAppCoordinator.swift; sourceTree = "<group>"; };
|
||||
5CDE60FEE95039CCCEEEE3B0 /* TimelineMediaPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewController.swift; sourceTree = "<group>"; };
|
||||
@@ -2694,6 +2697,7 @@
|
||||
C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
|
||||
C57DB49B8426AA721BF85D83 /* SpaceServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceServiceProxyProtocol.swift; sourceTree = "<group>"; };
|
||||
C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
C5CB64E711E86270B722FAF7 /* LiveLocationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationManagerTests.swift; sourceTree = "<group>"; };
|
||||
C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreen.swift; sourceTree = "<group>"; };
|
||||
C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = "<group>"; };
|
||||
C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
@@ -4867,6 +4871,7 @@
|
||||
DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */,
|
||||
FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */,
|
||||
C9AC2CC94FA06F728883B694 /* KnockRequestsListScreenViewModelTests.swift */,
|
||||
C5CB64E711E86270B722FAF7 /* LiveLocationManagerTests.swift */,
|
||||
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */,
|
||||
EED6D8956E554CEDFD4FE00D /* LocationSharingScreenViewModelTests.swift */,
|
||||
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */,
|
||||
@@ -5060,6 +5065,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
47F441A78A5CAA9E2937E463 /* KnockRequestsBannerView.swift */,
|
||||
5C60559596897AC65D2CB799 /* LiveLocationSharingBannerView.swift */,
|
||||
F48A2FA6814F824ABB4C07F3 /* RoomCallControlsToolbar.swift */,
|
||||
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */,
|
||||
4137900E28201C314C835C11 /* RoomScreenFooterView.swift */,
|
||||
@@ -7805,6 +7811,7 @@
|
||||
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */,
|
||||
BA48D6AFF6421D199148C0A1 /* KnockRequestsListScreenViewModelTests.swift in Sources */,
|
||||
CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */,
|
||||
CB6FBAF32B1305F914CF4225 /* LiveLocationManagerTests.swift in Sources */,
|
||||
0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */,
|
||||
F6D07D834279BE17D18A33E9 /* LocationSharingScreenViewModelTests.swift in Sources */,
|
||||
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
|
||||
@@ -8378,6 +8385,7 @@
|
||||
CD077E14FAADC444C5A80068 /* LiveLocationManagerProtocol.swift in Sources */,
|
||||
C8D0AC22E03F652118A2BB73 /* LiveLocationRoomTimelineItem.swift in Sources */,
|
||||
F7977C53B2B1D73030C69761 /* LiveLocationRoomTimelineView.swift in Sources */,
|
||||
9223E5F2A2CE0AFFDFF0AFFB /* LiveLocationSharingBannerView.swift in Sources */,
|
||||
6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */,
|
||||
D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */,
|
||||
256D76972BA3254F7CB7F88B /* LocationAnnotation.swift in Sources */,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A single banner item to be displayed in a `topBanners` overlay.
|
||||
/// An individual banner in the vertical stack of a `TopBannerLayer`.
|
||||
struct TopBannerItem {
|
||||
var banner: AnyView
|
||||
var isVisible: Bool
|
||||
@@ -18,18 +18,42 @@ struct TopBannerItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Z-axis banner slot displayed in a `topBanners` overlay. Each slot may
|
||||
/// contain one or more vertically stacked banners, each with its own
|
||||
/// visibility. The slot's overall visibility is derived from whether any of
|
||||
/// its vertical banners are visible. Later items in the `topBanners` array
|
||||
/// are overlayed on top of earlier ones (Z-axis).
|
||||
struct TopBannerLayer {
|
||||
var verticalBanners: [TopBannerItem]
|
||||
|
||||
var isVisible: Bool {
|
||||
verticalBanners.contains { $0.isVisible }
|
||||
}
|
||||
|
||||
/// Convenience initialiser for a single-banner slot.
|
||||
init(_ banner: some View, isVisible: Bool) {
|
||||
verticalBanners = [TopBannerItem(banner, isVisible: isVisible)]
|
||||
}
|
||||
|
||||
init(verticalBanners: [TopBannerItem]) {
|
||||
self.verticalBanners = verticalBanners
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Overlays the given banner view at the top edge of this view, using a
|
||||
/// slide from the top edge when `isVisible` is toggled.
|
||||
func topBanner(_ banner: some View, isVisible: Bool, footer: some View = EmptyView()) -> some View {
|
||||
topBanners([TopBannerItem(banner, isVisible: isVisible)], footer: footer)
|
||||
topBanners([TopBannerLayer(banner, isVisible: isVisible)], footer: footer)
|
||||
}
|
||||
|
||||
/// Overlays the given banner views at the top edge of this view. Each banner
|
||||
/// slides from the top edge based on its own `isVisible` flag. Later items in
|
||||
/// the array are overlayed on top of earlier ones. The footer is shared across
|
||||
/// all banners and displayed below the topmost visible banner.
|
||||
func topBanners(_ items: [TopBannerItem], footer: some View = EmptyView()) -> some View {
|
||||
/// Overlays the given Z-axis banner slots at the top edge of this view.
|
||||
/// Later items in the array are overlayed on top of earlier ones. Within
|
||||
/// each slot, visible vertical banners are stacked in a VStack and slide
|
||||
/// in/out from the top edge. The shadow and bottom padding are applied to
|
||||
/// the VStack of each slot. The footer is shared and displayed below the
|
||||
/// topmost visible slot.
|
||||
func topBanners(_ items: [TopBannerLayer], footer: some View = EmptyView()) -> some View {
|
||||
let anyBannerVisible = items.contains { $0.isVisible }
|
||||
return overlay(alignment: .top) {
|
||||
ZStack(alignment: .top) {
|
||||
@@ -38,8 +62,19 @@ extension View {
|
||||
ZStack(alignment: .top) {
|
||||
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
|
||||
if item.isVisible {
|
||||
item.banner
|
||||
.transition(.move(edge: .top))
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(item.verticalBanners.enumerated()), id: \.offset) { _, vBanner in
|
||||
if vBanner.isVisible {
|
||||
vBanner.banner
|
||||
.transition(.move(edge: .top))
|
||||
}
|
||||
}
|
||||
}
|
||||
.compositingGroup()
|
||||
.shadow(color: Color(red: 0.11, green: 0.11, blue: 0.13).opacity(0.1), radius: 12, x: 0, y: 4)
|
||||
// To include the shadow in the size
|
||||
.padding(.bottom, 28)
|
||||
.transition(.move(edge: .top))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +88,7 @@ extension View {
|
||||
.hidden()
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.animation(.elementDefault, value: items.map(\.isVisible))
|
||||
.animation(.elementDefault, value: items.map { $0.verticalBanners.map(\.isVisible) })
|
||||
.clipped()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ enum TestablePreviewsDictionary {
|
||||
"LegalInformationScreen_Previews" : LegalInformationScreen_Previews.self,
|
||||
"LinkNewDeviceScreen_Previews" : LinkNewDeviceScreen_Previews.self,
|
||||
"LiveLocationRoomTimelineView_Previews" : LiveLocationRoomTimelineView_Previews.self,
|
||||
"LiveLocationSharingBannerView_Previews" : LiveLocationSharingBannerView_Previews.self,
|
||||
"LoadableImage_Previews" : LoadableImage_Previews.self,
|
||||
"LocationMarkerView_Previews" : LocationMarkerView_Previews.self,
|
||||
"LocationPickerSheet_Previews" : LocationPickerSheet_Previews.self,
|
||||
|
||||
@@ -192,6 +192,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentThreadList)
|
||||
case .displayThread(let threadRootEventID, let focussedEventID):
|
||||
actionsSubject.send(.presentThread(threadRootEventID: threadRootEventID, focussedEventID: focussedEventID))
|
||||
case .stopLiveLocationSharing:
|
||||
Task { [weak self] in await self?.timelineViewModel.stopLiveLocationSharing() }
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -21,6 +21,7 @@ enum RoomScreenViewModelAction: Equatable {
|
||||
case displayKnockRequests
|
||||
case displayRoom(roomID: String, via: [String])
|
||||
case displayMessageForwarding(MessageForwardingItem)
|
||||
case stopLiveLocationSharing
|
||||
}
|
||||
|
||||
enum RoomScreenViewAction {
|
||||
@@ -34,6 +35,7 @@ enum RoomScreenViewAction {
|
||||
case viewKnockRequests
|
||||
case displaySuccessorRoom
|
||||
case displayThreadList
|
||||
case tappedStopLiveLocation
|
||||
}
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
@@ -48,6 +50,8 @@ struct RoomScreenViewState: BindableState {
|
||||
!pinnedEventsBannerState.isEmpty && lastScrollDirection != .top
|
||||
}
|
||||
|
||||
var isSharingLiveLocation = false
|
||||
|
||||
var canSendMessage = true
|
||||
|
||||
/// Whether or not starting a call is supported.
|
||||
|
||||
@@ -122,6 +122,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
actionsSubject.send(.displayRoom(roomID: successorID, via: Array(serverNames)))
|
||||
case .displayThreadList:
|
||||
actionsSubject.send(.displayThreadList)
|
||||
case .tappedStopLiveLocation:
|
||||
actionsSubject.send(.stopLiveLocationSharing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +174,15 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
appSettings.$knockingEnabled
|
||||
.weakAssign(to: \.state.isKnockingEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
appSettings.$liveLocationSharingEnabled
|
||||
.combineLatest(appSettings.$liveLocationSharingTimeoutDatesByRoomID)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isEnabled, timeoutDatesByRoomID in
|
||||
guard let self else { return }
|
||||
state.isSharingLiveLocation = isEnabled && timeoutDatesByRoomID.keys.contains(roomProxy.id)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
roomProxy.infoPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
||||
@@ -28,9 +28,6 @@ struct KnockRequestsBannerView: View {
|
||||
mainContent
|
||||
.padding(16)
|
||||
.background(.compound.bgCanvasDefaultLevel1, in: RoundedRectangle(cornerRadius: 12))
|
||||
.compositingGroup()
|
||||
.shadow(color: Color(red: 0.11, green: 0.11, blue: 0.13).opacity(0.1), radius: 12, x: 0, y: 4)
|
||||
.padding(.bottom, 28)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
@@ -202,6 +199,14 @@ struct KnockRequestsBannerView_Previews: PreviewProvider, TestablePreview {
|
||||
]
|
||||
|
||||
static var previews: some View {
|
||||
allPreviews
|
||||
.padding()
|
||||
.background(.gray)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static var allPreviews: some View {
|
||||
KnockRequestsBannerView(requests: singleRequest) { } onAccept: { _ in } onViewAll: { }
|
||||
.previewDisplayName("Single Request")
|
||||
// swiftlint:disable:next trailing_closure
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Copyright 2025 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 Compound
|
||||
import SwiftUI
|
||||
|
||||
struct LiveLocationSharingBannerView: View {
|
||||
var onStop: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 9) {
|
||||
CompoundIcon(\.locationPinSolid, size: .medium, relativeTo: .compound.bodyMDSemibold)
|
||||
.foregroundColor(Color.compound.iconSuccessPrimary)
|
||||
.accessibilityHidden(true)
|
||||
Text(L10n.screenRoomLiveLocationBanner)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
Spacer()
|
||||
Button(L10n.actionStop, role: .destructive, action: onStop)
|
||||
.buttonStyle(.compound(.primary, size: .small))
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
.padding(.horizontal, 15)
|
||||
.background(Color.compound.bgCanvasDefault)
|
||||
.overlay(alignment: .top) { Color.compound.separatorPrimary.frame(height: 1) }
|
||||
.overlay(alignment: .bottom) { Color.compound.separatorPrimary.frame(height: 1) }
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveLocationSharingBannerView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
LiveLocationSharingBannerView { }
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -23,9 +23,6 @@ struct PinnedItemsBannerView: View {
|
||||
.padding(.vertical, 16)
|
||||
.padding(.leading, 16)
|
||||
.background(Color.compound.bgCanvasDefault)
|
||||
.shadow(color: Color(red: 0.11, green: 0.11, blue: 0.13).opacity(0.1), radius: 12, x: 0, y: 4)
|
||||
// To include the shadow in the size
|
||||
.padding(.bottom, 28)
|
||||
}
|
||||
|
||||
private var mainButton: some View {
|
||||
@@ -111,5 +108,8 @@ struct PinnedItemsBannerView_Previews: PreviewProvider, TestablePreview {
|
||||
onMainButtonTap: { },
|
||||
onViewAllButtonTap: { })
|
||||
}
|
||||
.padding()
|
||||
.background(.gray)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,17 +35,27 @@ struct RoomScreen: View {
|
||||
}
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.topBanners([
|
||||
TopBannerItem(pinnedItemsBanner, isVisible: context.viewState.shouldShowPinnedEventsBanner && !isVoiceOverEnabled),
|
||||
// This can overlay on top of the pinnedItemsBanner
|
||||
TopBannerItem(knockRequestsBanner, isVisible: context.viewState.shouldSeeKnockRequests)
|
||||
TopBannerLayer(verticalBanners: [
|
||||
TopBannerItem(pinnedItemsBanner, isVisible: context.viewState.shouldShowPinnedEventsBanner && !isVoiceOverEnabled),
|
||||
TopBannerItem(liveLocationBanner, isVisible: context.viewState.isSharingLiveLocation && !isVoiceOverEnabled)
|
||||
]),
|
||||
// This can overlay on top of the stacked banners
|
||||
TopBannerLayer(knockRequestsBanner, isVisible: context.viewState.shouldSeeKnockRequests)
|
||||
], footer: dateBadge)
|
||||
.safeAreaInset(edge: .top) {
|
||||
// When VoiceOver is enabled, the table view isn't reversed and the scroll gestures
|
||||
// don't trigger meaning the banner never hides itself and so the .overlay layout
|
||||
// above permanently obscures the top of the timeline. So whenever VoiceOver is
|
||||
// enabled we use a safe area inset to vertically stack it above the timeline.
|
||||
if context.viewState.shouldShowPinnedEventsBanner, isVoiceOverEnabled {
|
||||
pinnedItemsBanner
|
||||
if context.viewState.shouldShowPinnedEventsBanner || context.viewState.isSharingLiveLocation, isVoiceOverEnabled {
|
||||
VStack(spacing: 0) {
|
||||
if context.viewState.shouldShowPinnedEventsBanner {
|
||||
pinnedItemsBanner
|
||||
}
|
||||
if context.viewState.isSharingLiveLocation {
|
||||
liveLocationBanner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
@@ -76,6 +86,12 @@ struct RoomScreen: View {
|
||||
.sentryTrace("\(Self.self)")
|
||||
}
|
||||
|
||||
private var liveLocationBanner: some View {
|
||||
LiveLocationSharingBannerView {
|
||||
context.send(viewAction: .tappedStopLiveLocation)
|
||||
}
|
||||
}
|
||||
|
||||
private var pinnedItemsBanner: some View {
|
||||
PinnedItemsBannerView(state: context.viewState.pinnedEventsBannerState,
|
||||
onMainButtonTap: { context.send(viewAction: .tappedPinnedEventsBanner) },
|
||||
|
||||
@@ -70,7 +70,7 @@ enum TimelineViewAction {
|
||||
case handlePollAction(TimelineViewPollAction)
|
||||
case handleAudioPlayerAction(TimelineAudioPlayerAction)
|
||||
|
||||
case stopLiveLocationSharing
|
||||
case stopLiveLocationSharing(TimelineItemIdentifier)
|
||||
|
||||
/// Focus the timeline onto the specified event ID (switching to a detached timeline if needed).
|
||||
case focusOnEventID(String)
|
||||
@@ -143,6 +143,8 @@ struct TimelineViewState: BindableState {
|
||||
var mapTilerConfiguration: MapTilerConfiguration
|
||||
|
||||
var enableKeyShareOnInvite: Bool
|
||||
|
||||
var stoppedLiveLocationIDs: Set<TimelineItemIdentifier> = []
|
||||
|
||||
var bindings: TimelineViewStateBindings
|
||||
}
|
||||
|
||||
@@ -198,8 +198,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
handlePollAction(pollAction)
|
||||
case .handleAudioPlayerAction(let audioPlayerAction):
|
||||
handleAudioPlayerAction(audioPlayerAction)
|
||||
case .stopLiveLocationSharing:
|
||||
Task { await userSession.liveLocationManager.stopLiveLocation(roomID: state.roomID) }
|
||||
case .stopLiveLocationSharing(let id):
|
||||
state.stoppedLiveLocationIDs.insert(id)
|
||||
Task { await stopLiveLocationSharing() }
|
||||
case .focusOnEventID(let eventID):
|
||||
Task { await focusOnEvent(eventID: eventID) }
|
||||
case .focusLive:
|
||||
@@ -276,6 +277,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func stopLiveLocationSharing() async {
|
||||
await userSession.liveLocationManager.stopLiveLocation(roomID: roomProxy.id)
|
||||
}
|
||||
|
||||
func makeForwardingItem(for itemID: TimelineItemIdentifier) async -> MessageForwardingItem? {
|
||||
guard let content = await timelineController.messageEventContent(for: itemID) else { return nil }
|
||||
return .init(id: itemID, roomID: roomProxy.id, content: content)
|
||||
|
||||
@@ -18,7 +18,8 @@ protocol TimelineViewModelProtocol {
|
||||
func process(composerAction: ComposerToolbarViewModelAction)
|
||||
/// Updates the timeline to show and highlight the item with the corresponding event ID.
|
||||
func focusOnEvent(eventID: String) async
|
||||
|
||||
/// Stops the current live location sharing
|
||||
func stopLiveLocationSharing() async
|
||||
/// Handles getting the content to forward an item given its item ID.
|
||||
func makeForwardingItem(for itemID: TimelineItemIdentifier) async -> MessageForwardingItem?
|
||||
}
|
||||
|
||||
+3
-3
@@ -15,10 +15,10 @@ struct LiveLocationRoomTimelineView: View {
|
||||
let timelineItem: LiveLocationRoomTimelineItem
|
||||
private let currentDate: Date
|
||||
|
||||
init(currentDate: Date = .now, timelineItem: LiveLocationRoomTimelineItem) {
|
||||
init(currentDate: Date = .now, timelineItem: LiveLocationRoomTimelineItem, isStopped: Bool = false) {
|
||||
self.currentDate = currentDate
|
||||
self.timelineItem = timelineItem
|
||||
_hasExpired = State(initialValue: currentDate >= timelineItem.content.timeoutDate)
|
||||
_hasExpired = State(initialValue: isStopped || currentDate >= timelineItem.content.timeoutDate)
|
||||
}
|
||||
|
||||
/// A publisher that fires once when the timeoutDate is reached, setting `hasExpired` to true.
|
||||
@@ -191,7 +191,7 @@ struct LiveLocationRoomTimelineView: View {
|
||||
|
||||
private func stop() {
|
||||
hasExpired = true
|
||||
context?.send(viewAction: .stopLiveLocationSharing)
|
||||
context?.send(viewAction: .stopLiveLocationSharing(timelineItem.id))
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
@@ -60,6 +60,12 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
||||
}
|
||||
|
||||
func startLiveLocation(roomID: String, duration: Duration) async -> Result<Void, LiveLocationManagerError> {
|
||||
// Stop any existing session for this room first (e.g. one started from a different device)
|
||||
// before starting a new one.
|
||||
if appSettings.liveLocationSharingTimeoutDatesByRoomID[roomID] != nil {
|
||||
await stopLiveLocation(roomID: roomID)
|
||||
}
|
||||
|
||||
guard case .joined(let roomProxy) = await clientProxy.roomForIdentifier(roomID) else {
|
||||
MXLog.error("Failed to resolve joined room for identifier: \(roomID)")
|
||||
return .failure(.roomNotJoined)
|
||||
@@ -79,17 +85,23 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana
|
||||
}
|
||||
|
||||
func stopLiveLocation(roomID: String) async {
|
||||
// Best effort: send the stop event to the room regardless of tracking state.
|
||||
if let roomProxy = await resolveRoomProxy(for: roomID) {
|
||||
var roomProxy: JoinedRoomProxyProtocol?
|
||||
let cachedRoomProxy = activeRoomProxies[roomID]
|
||||
appSettings.liveLocationSharingTimeoutDatesByRoomID.removeValue(forKey: roomID)
|
||||
|
||||
if let cachedRoomProxy {
|
||||
roomProxy = cachedRoomProxy
|
||||
// Best effort: send the stop event to the room regardless of tracking state.
|
||||
} else if case let .joined(fetchedRoomProxy) = await clientProxy.roomForIdentifier(roomID) {
|
||||
roomProxy = fetchedRoomProxy
|
||||
}
|
||||
|
||||
if let roomProxy {
|
||||
let result = await roomProxy.stopLiveLocationShare()
|
||||
if case .failure(let error) = result {
|
||||
MXLog.error("Failed to stop live location share in room \(roomID): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Always clean up locally.
|
||||
appSettings.liveLocationSharingTimeoutDatesByRoomID.removeValue(forKey: roomID)
|
||||
activeRoomProxies.removeValue(forKey: roomID)
|
||||
}
|
||||
|
||||
// MARK: - CLLocationManagerDelegate
|
||||
|
||||
@@ -76,7 +76,7 @@ struct RoomTimelineItemView: View {
|
||||
case .callNotification(let item):
|
||||
CallNotificationRoomTimelineView(timelineItem: item)
|
||||
case .liveLocation(let item):
|
||||
LiveLocationRoomTimelineView(timelineItem: item)
|
||||
LiveLocationRoomTimelineView(timelineItem: item, isStopped: context?.viewState.stoppedLiveLocationIDs.contains(item.id) ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -603,6 +603,14 @@ extension PreviewTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func liveLocationSharingBannerView() async throws {
|
||||
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
|
||||
for (index, preview) in LiveLocationSharingBannerView_Previews._allPreviews.enumerated() {
|
||||
try await assertSnapshots(matching: preview, step: index)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func loadableImage() async throws {
|
||||
AppSettings.resetAllSettings() // Ensure this test's previews start with fresh settings.
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4cc21640dc10190ddfa345a83c8e4bedae2cca526f3a517980b917df1fd819c1
|
||||
size 92313
|
||||
oid sha256:1add71eec72332f3d0f4f56f45068eb44a27290d16b5699ae16d523180b21315
|
||||
size 30977
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4119ef48dd28330cbe9989a5e2842795b2e14874a7e03d353267a7530041b475
|
||||
size 93248
|
||||
oid sha256:09e6a9bfffd30aed0d98e81d0f83363517948f42b178bdaa697839474c0b28d2
|
||||
size 31918
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:123673378b70eba1774e7f738d0b9704344158dc3ac6a8316724628a6317e161
|
||||
size 50246
|
||||
oid sha256:183f69e4d8bff9139f39194fd86ab630806376f11b0d8e48b7fdbc34df5ba5cf
|
||||
size 22773
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3549c8ec426dbf2210ff9e7c0969ecdb7dc5b15d605d157b3754a9c09b0a8991
|
||||
size 49872
|
||||
oid sha256:2fb13539b94dc0e8ffec3949616b1f1d08b0c01cfeeca864d3a87f37cd7995a2
|
||||
size 23345
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dca4b68102343046709e0d3430e0e2409b04f47fa6a35e8df489246be29500e2
|
||||
size 95109
|
||||
oid sha256:7ed343dc6fdfe8fe86931ac57bb5de221b893e67ea6740f09b03da2f0d5022f6
|
||||
size 33984
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6f20f61ab302e2a69373646f3e713e5bfe729d317ff19cf5362d2b67cf7b844d
|
||||
size 98462
|
||||
oid sha256:c72a9cb9efee8db318182df426a39542100cb8f634646364f5ab2371e2eee743
|
||||
size 37324
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:32cccc3b9096777d7385bb34b4f76d1ae023a4b4d96be3021dd2de7eae4b6526
|
||||
size 53001
|
||||
oid sha256:8df4c96c5f7ed1e16b00eed25ee489779b82108e616cf5fcd347b02479018fe2
|
||||
size 25336
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f6366953f27bdf503d01e7b8ff34396ac7ba5e514d93723aab6870229979a759
|
||||
size 61857
|
||||
oid sha256:41ae37d20af35cc9eafbc37a3a58551c8b08a7fce2f2e5c8b1016540d4a04391
|
||||
size 32666
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f0ee54a8bd1364f456cedf7c8a5f6f0652d4e26f3d34bf3a14b8fa564ae41028
|
||||
size 97300
|
||||
oid sha256:1b7b864242e784d8ddc6f95be8f0e9e27c81b4fac495c587381b0602d520a8a5
|
||||
size 36897
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cec2564a143dceb7ea8c6b23e1bb2f2f4626af08ae11dfe6b4365a4c95f96efa
|
||||
size 102100
|
||||
oid sha256:435a74365d46ac1366bf2c6343d349cce8fec2766cb09b2d61c7f92187a4e285
|
||||
size 41658
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:af1e8fb046d261c1e7274cfd61033e1fef539b8f13af8b14045d36c237cf458c
|
||||
size 52975
|
||||
oid sha256:410bbb9cf2020e28ae5da029aa7aa370c49351eb706692db7e1e976cde373a52
|
||||
size 25816
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49301d759419c2418fe628957e6dbd14702a0625d6307c2dcc1f4a2dc1d715ef
|
||||
size 63378
|
||||
oid sha256:573d09b7aba1e7a20a20090ad2baf4236f7024bdb7d266383a8ccbd68c525eec
|
||||
size 35993
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f5a7b449241c47f37a05f16196e9f1478e861dba372cd740d8113471b4c64573
|
||||
size 92965
|
||||
oid sha256:4e4353be9b16318ec5774ed9ce19bcd7092dcb575958ed003ece8a7e8ce1e6f9
|
||||
size 32609
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a8c2022369fe6c681b6645fcc87485a390a548743b03ff4125f8aa01ff50985f
|
||||
size 97357
|
||||
oid sha256:006f2ac00dc322f133db40b9c22d7e9a74bf06420487a333d01d4f12aa41ba77
|
||||
size 36956
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:75f23e3dd857927ff19e65481a429f0b066543d938b6418cd742b2462d038817
|
||||
size 49340
|
||||
oid sha256:249790076d8003df15316cfa253a502fd44fb7dc66a6642dcf4cdcdab0b9d9e4
|
||||
size 22412
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4c27af06d422419337c232ea0e67f23ae5efafc42d17986e56d141aa27ea71e4
|
||||
size 56198
|
||||
oid sha256:51461496517a802c737c245e510de941a176cf93f2759daf2c9e2e4053a41b36
|
||||
size 28381
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1eca3d3966ed170902068948b0e06792f5a86a957bc5e9fa4ee12367db0ecfdc
|
||||
size 110095
|
||||
oid sha256:b6316e6324dbd72e72dc0eb8cf09b80e7788c091746fbff2dd7f62220b146984
|
||||
size 52744
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d3fbf972978286e7a63cbe2edbeb1c3af50ecb37752706307dc0b276a73d60fd
|
||||
size 114871
|
||||
oid sha256:bf1476690cc3cccf2ce3966b6791510a44d65ba1370563fcc3a560b6fc548aba
|
||||
size 57490
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e819430839323b67d25f477fe9de16caf0d4e3cdeb6950edcc105e6e1b95413a
|
||||
size 65778
|
||||
oid sha256:2b94ac6d13906cddffe740a57b5c044adc9389a9d7514fe576d2e0e8080c923f
|
||||
size 40671
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aff4b437d86ccd489cc49b675befcfb98660c4abfdca7d7e8a57d5f57877ce77
|
||||
size 75574
|
||||
oid sha256:7c637b790aff4bb5341be902d80681fc04618ac94816dc7f0f675048c0849645
|
||||
size 50880
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ae5fb963478cbc8abf7dc4732ea8f4295ba86efd9e917d2e167378a6e06520f6
|
||||
size 14937
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:45a96c2f592cbc5cf7a2f1282679bc4b7d592e903df161f0e10c853b4401cd6e
|
||||
size 18159
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:acc5d3b386e57b63e6e2386b96d3cde0eb1afa9b0cc8fc914e1495882e39b691
|
||||
size 10977
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f6126e63cf2456619c7e6d2212e6fbf4b6f5af7426df7241ca2039314827da64
|
||||
size 15642
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f727d2737e536d158ec5488ef7ae670bdf17b4f10789f560340b3c7b36cb2c76
|
||||
size 143061
|
||||
oid sha256:aaedf20685450b28deb7e16673a0f26a888fa345a55c603a68ac3d781e1aea42
|
||||
size 79416
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:784b2e39ddcd2c1c04669c1e9dd00dddb9388b738afa283732bf389304d7b398
|
||||
size 170747
|
||||
oid sha256:4a8de9fae72c3bbcf8a5d205f989ecaa2c3d6313a4597a6b93027651e4cb2f3c
|
||||
size 99670
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79abbb9b6c53da934a3451ed8d61d89c441500a5f6986e07c8fc9c32784afab6
|
||||
size 87380
|
||||
oid sha256:0f627cbfbd1c93ad7c77dd41ef3020f3c652ef53b64ebc51bfa30cd364538168
|
||||
size 52157
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e196ada8c3f0f808868a80330e7a389d658519903cb51680d27ddfa4bd33ea60
|
||||
size 101585
|
||||
oid sha256:fc3309c3d3e7f290cc21e04d0cb239e8067e961bd4c4c56c9c102ec448852306
|
||||
size 57519
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
final class LiveLocationManagerTests {
|
||||
private var clientProxy: ClientProxyMock!
|
||||
private var manager: LiveLocationManager!
|
||||
|
||||
private let appSettings: AppSettings
|
||||
|
||||
init() {
|
||||
AppSettings.resetAllSettings()
|
||||
appSettings = AppSettings()
|
||||
clientProxy = ClientProxyMock(.init())
|
||||
manager = LiveLocationManager(clientProxy: clientProxy, appSettings: appSettings)
|
||||
}
|
||||
|
||||
deinit {
|
||||
AppSettings.resetAllSettings()
|
||||
}
|
||||
|
||||
// MARK: - startLiveLocation
|
||||
|
||||
@Test
|
||||
func startLiveLocationWithNoExistingSession() async throws {
|
||||
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||
|
||||
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
||||
|
||||
try result.get()
|
||||
#expect(roomProxy.startLiveLocationShareDurationCalled)
|
||||
#expect(!roomProxy.stopLiveLocationShareCalled)
|
||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] != nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func startLiveLocationWithExistingSessionStopsItFirst() async throws {
|
||||
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300)
|
||||
|
||||
var callOrder: [String] = []
|
||||
roomProxy.stopLiveLocationShareClosure = {
|
||||
callOrder.append("stop")
|
||||
return .success(())
|
||||
}
|
||||
roomProxy.startLiveLocationShareDurationClosure = { _ in
|
||||
callOrder.append("start")
|
||||
return .success(())
|
||||
}
|
||||
|
||||
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(600))
|
||||
|
||||
try result.get()
|
||||
#expect(callOrder == ["stop", "start"])
|
||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] != nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func startLiveLocationDoesNotStopSessionForOtherRoom() async {
|
||||
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||
|
||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] = Date().addingTimeInterval(300)
|
||||
|
||||
_ = await manager.startLiveLocation(roomID: "!room1:matrix.org", duration: .seconds(300))
|
||||
|
||||
#expect(!roomProxy.stopLiveLocationShareCalled)
|
||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] != nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func startLiveLocationWhenRoomNotJoined() async {
|
||||
clientProxy.roomForIdentifierClosure = { _ in nil }
|
||||
|
||||
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
||||
|
||||
#expect(throws: LiveLocationManagerError.roomNotJoined) { try result.get() }
|
||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func startLiveLocationWhenStartShareFails() async {
|
||||
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
||||
roomProxy.startLiveLocationShareDurationReturnValue = .failure(.sdkError(RoomProxyMockError.generic))
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||
|
||||
let result = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300))
|
||||
|
||||
#expect(throws: LiveLocationManagerError.startFailed) { try result.get() }
|
||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func startLiveLocationStoresTimeoutDate() async throws {
|
||||
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||
|
||||
let duration = Duration.seconds(300)
|
||||
let beforeStart = Date()
|
||||
_ = await manager.startLiveLocation(roomID: "!room:matrix.org", duration: duration)
|
||||
let afterStart = Date()
|
||||
|
||||
let storedTimeout = appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"]
|
||||
let expectedMinTimeout = beforeStart.addingTimeInterval(TimeInterval(duration.seconds))
|
||||
let expectedMaxTimeout = afterStart.addingTimeInterval(TimeInterval(duration.seconds))
|
||||
|
||||
try #expect((expectedMinTimeout...expectedMaxTimeout).contains(#require(storedTimeout)))
|
||||
}
|
||||
|
||||
// MARK: - stopLiveLocation
|
||||
|
||||
@Test
|
||||
func stopLiveLocationWhenSessionExists() async {
|
||||
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] = Date().addingTimeInterval(300)
|
||||
|
||||
await manager.stopLiveLocation(roomID: "!room:matrix.org")
|
||||
|
||||
#expect(roomProxy.stopLiveLocationShareCalled)
|
||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room:matrix.org"] == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func stopLiveLocationWhenNoSession() async {
|
||||
let roomProxy = makeRoomProxy(roomID: "!room:matrix.org")
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||
|
||||
await manager.stopLiveLocation(roomID: "!room:matrix.org")
|
||||
|
||||
#expect(roomProxy.stopLiveLocationShareCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
func stopLiveLocationDoesNotRemoveOtherSessions() async {
|
||||
let roomProxy = makeRoomProxy(roomID: "!room1:matrix.org")
|
||||
clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) }
|
||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room1:matrix.org"] = Date().addingTimeInterval(300)
|
||||
appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] = Date().addingTimeInterval(300)
|
||||
|
||||
await manager.stopLiveLocation(roomID: "!room1:matrix.org")
|
||||
|
||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room1:matrix.org"] == nil)
|
||||
#expect(appSettings.liveLocationSharingTimeoutDatesByRoomID["!room2:matrix.org"] != nil)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func makeRoomProxy(roomID: String) -> JoinedRoomProxyMock {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(id: roomID))
|
||||
roomProxy.startLiveLocationShareDurationReturnValue = .success(())
|
||||
roomProxy.stopLiveLocationShareReturnValue = .success(())
|
||||
return roomProxy
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user