mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +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:
@@ -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) },
|
||||
|
||||
Reference in New Issue
Block a user