mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
reactively determine with a timer if the lls timeline item has expired or not.
Move mockToday410/420/430 to private Date extension in LiveLocationRoomTimelineView Agent-Logs-Url: https://github.com/element-hq/element-x-ios/sessions/d2136882-1acc-455d-a363-815e0fb73e89 Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> fix
This commit is contained in:
@@ -79,12 +79,6 @@ extension Date {
|
||||
formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
|
||||
/// A fixed date representing today at 4:20 AM, used for mocks and previews.
|
||||
static var mockToday420: Date {
|
||||
// swiftlint:disable:next force_unwrap
|
||||
Calendar.current.date(bySettingHour: 4, minute: 20, second: 0, of: .now)!
|
||||
}
|
||||
|
||||
/// A fixed date used for mocks, previews etc.
|
||||
static var mock: Date {
|
||||
DateComponents(calendar: .current, year: 2007, month: 1, day: 9, hour: 9, minute: 41).date ?? .now
|
||||
|
||||
+102
-28
@@ -5,12 +5,41 @@
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct LiveLocationRoomTimelineView: View {
|
||||
@Environment(\.timelineContext) private var context: TimelineViewModel.Context!
|
||||
@State private var hasExpired: Bool
|
||||
let timelineItem: LiveLocationRoomTimelineItem
|
||||
private let currentDate: Date
|
||||
|
||||
init(currentDate: Date = .now, timelineItem: LiveLocationRoomTimelineItem) {
|
||||
self.currentDate = currentDate
|
||||
self.timelineItem = timelineItem
|
||||
_hasExpired = State(initialValue: currentDate >= timelineItem.content.timeoutDate)
|
||||
}
|
||||
|
||||
/// A publisher that fires once when the timeoutDate is reached, setting `hasExpired` to true.
|
||||
private var timeoutPublisher: AnyPublisher<Void, Never> {
|
||||
guard timelineItem.content.isLive else {
|
||||
return Empty().eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let delay = timelineItem.content.timeoutDate.timeIntervalSinceNow
|
||||
guard delay > 0 else {
|
||||
return Empty().eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Just(())
|
||||
.delay(for: .seconds(delay), scheduler: DispatchQueue.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private var isLive: Bool {
|
||||
timelineItem.content.isLive && !hasExpired
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
@@ -30,7 +59,7 @@ struct LiveLocationRoomTimelineView: View {
|
||||
.frame(maxHeight: mapMaxHeight)
|
||||
.aspectRatio(mapAspectRatio, contentMode: .fit)
|
||||
.overlay {
|
||||
if !timelineItem.content.isLive {
|
||||
if !isLive {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.inset(by: 0.5)
|
||||
.stroke(Color.compound.borderDisabled)
|
||||
@@ -39,7 +68,7 @@ struct LiveLocationRoomTimelineView: View {
|
||||
.onTapGesture {
|
||||
guard context.viewState.mapTilerConfiguration.isEnabled,
|
||||
timelineItem.content.lastGeoURI != nil,
|
||||
timelineItem.content.isLive else {
|
||||
isLive else {
|
||||
return
|
||||
}
|
||||
context.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
||||
@@ -48,19 +77,11 @@ struct LiveLocationRoomTimelineView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var mapView: some View {
|
||||
if timelineItem.content.isLive {
|
||||
if let geoURI = timelineItem.content.lastGeoURI {
|
||||
MapLibreStaticMapView(geoURI: geoURI,
|
||||
mapURLBuilder: context.viewState.mapTilerConfiguration,
|
||||
attributionPlacement: .topLeft,
|
||||
mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
|
||||
LocationMarkerView(kind: .liveUser(.init(sender: timelineItem.sender)),
|
||||
mediaProvider: context.mediaProvider)
|
||||
if isLive {
|
||||
liveContent
|
||||
.onReceive(timeoutPublisher) { _ in
|
||||
hasExpired = true
|
||||
}
|
||||
} else {
|
||||
Image(asset: Asset.Images.mapBlurred)
|
||||
.resizable()
|
||||
}
|
||||
} else {
|
||||
ZStack {
|
||||
Image(asset: Asset.Images.placeholderMap)
|
||||
@@ -70,16 +91,32 @@ struct LiveLocationRoomTimelineView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var liveContent: some View {
|
||||
if let geoURI = timelineItem.content.lastGeoURI {
|
||||
MapLibreStaticMapView(geoURI: geoURI,
|
||||
mapURLBuilder: context.viewState.mapTilerConfiguration,
|
||||
attributionPlacement: .topLeft,
|
||||
mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
|
||||
LocationMarkerView(kind: .liveUser(.init(sender: timelineItem.sender)),
|
||||
mediaProvider: context.mediaProvider)
|
||||
}
|
||||
} else {
|
||||
Image(asset: Asset.Images.mapBlurred)
|
||||
.resizable()
|
||||
}
|
||||
}
|
||||
|
||||
private var liveLocationStateString: String {
|
||||
timelineItem.content.isLive ? L10n.commonLiveLocation : L10n.commonLiveLocationEnded
|
||||
isLive ? L10n.commonLiveLocation : L10n.commonLiveLocationEnded
|
||||
}
|
||||
|
||||
private var liveLocationStateColor: Color {
|
||||
timelineItem.content.isLive ? .compound.textPrimary : .compound.textSecondary
|
||||
isLive ? .compound.textPrimary : .compound.textSecondary
|
||||
}
|
||||
|
||||
private var liveLocationIconColor: Color {
|
||||
if timelineItem.content.isLive {
|
||||
if isLive {
|
||||
timelineItem.content.lastGeoURI != nil ? .compound.iconAccentPrimary : .compound.iconSecondary
|
||||
} else {
|
||||
.compound.iconDisabled
|
||||
@@ -87,7 +124,7 @@ struct LiveLocationRoomTimelineView: View {
|
||||
}
|
||||
|
||||
private var liveLocationBackgroundColor: Color {
|
||||
timelineItem.content.isLive ? .compound.bgCanvasDefault : .compound.bgSubtleSecondary
|
||||
isLive ? .compound.bgCanvasDefault : .compound.bgSubtleSecondary
|
||||
}
|
||||
|
||||
private var blurBackground: some View {
|
||||
@@ -97,7 +134,7 @@ struct LiveLocationRoomTimelineView: View {
|
||||
}
|
||||
|
||||
private var infoIcon: KeyPath<CompoundIcons, Image> {
|
||||
if timelineItem.content.lastGeoURI != nil || !timelineItem.content.isLive {
|
||||
if timelineItem.content.lastGeoURI != nil || !isLive {
|
||||
\.locationPinSolid
|
||||
} else {
|
||||
\.spinner
|
||||
@@ -112,7 +149,7 @@ struct LiveLocationRoomTimelineView: View {
|
||||
.background(liveLocationBackgroundColor)
|
||||
.cornerRadius(8)
|
||||
.overlay {
|
||||
if timelineItem.content.isLive {
|
||||
if isLive {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.inset(by: 0.5)
|
||||
.stroke(Color.compound.iconQuaternaryAlpha)
|
||||
@@ -125,7 +162,7 @@ struct LiveLocationRoomTimelineView: View {
|
||||
.foregroundStyle(liveLocationStateColor)
|
||||
.font(.compound.bodySMSemibold)
|
||||
|
||||
if timelineItem.content.isLive {
|
||||
if isLive {
|
||||
Text(L10n.commonEndsAt(timelineItem.content.timeoutDate.formattedExpiration()))
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.font(.compound.bodySM)
|
||||
@@ -135,7 +172,7 @@ struct LiveLocationRoomTimelineView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
if timelineItem.content.isLive, timelineItem.isOutgoing {
|
||||
if isLive, timelineItem.isOutgoing {
|
||||
Button { } label: {
|
||||
CompoundIcon(\.stop, size: .small, relativeTo: .compound.bodySMSemibold)
|
||||
.foregroundStyle(.compound.iconOnSolidPrimary)
|
||||
@@ -156,6 +193,26 @@ struct LiveLocationRoomTimelineView: View {
|
||||
private let mapMaxHeight: Double = 300
|
||||
}
|
||||
|
||||
private extension Date {
|
||||
/// A fixed date representing today at 4:10 AM, used for mocks and previews.
|
||||
static var mockToday410: Date {
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
Calendar.current.date(bySettingHour: 4, minute: 10, second: 0, of: .now)!
|
||||
}
|
||||
|
||||
/// A fixed date representing today at 4:20 AM, used for mocks and previews.
|
||||
static var mockToday420: Date {
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
Calendar.current.date(bySettingHour: 4, minute: 20, second: 0, of: .now)!
|
||||
}
|
||||
|
||||
/// A fixed date representing today at 4:30 AM, used for mocks and previews.
|
||||
static var mockToday430: Date {
|
||||
// swiftlint:disable:next force_unwrapping
|
||||
Calendar.current.date(bySettingHour: 4, minute: 30, second: 0, of: .now)!
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveLocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
|
||||
@@ -174,7 +231,8 @@ struct LiveLocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
@ViewBuilder
|
||||
static var states: some View {
|
||||
// No location yet (beacon not yet received)
|
||||
LiveLocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
LiveLocationRoomTimelineView(currentDate: .mockToday410,
|
||||
timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
@@ -185,7 +243,8 @@ struct LiveLocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
lastGeoURI: nil)))
|
||||
|
||||
// With a known location
|
||||
LiveLocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
LiveLocationRoomTimelineView(currentDate: .mockToday410,
|
||||
timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
@@ -194,8 +253,9 @@ struct LiveLocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
content: .init(isLive: true,
|
||||
timeoutDate: .mockToday420,
|
||||
lastGeoURI: .init(latitude: 41.902782, longitude: 12.496366))))
|
||||
// Expired live location
|
||||
LiveLocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
// Stopped live location
|
||||
LiveLocationRoomTimelineView(currentDate: .mockToday410,
|
||||
timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
@@ -205,8 +265,21 @@ struct LiveLocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
timeoutDate: .mockToday420,
|
||||
lastGeoURI: .init(latitude: 41.902782, longitude: 12.496366))))
|
||||
|
||||
// Expired live location
|
||||
LiveLocationRoomTimelineView(currentDate: .mockToday430,
|
||||
timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
sender: .init(id: "@bob:matrix.org", displayName: "Bob", avatarURL: .mockMXCUserAvatar),
|
||||
content: .init(isLive: true,
|
||||
timeoutDate: .mockToday420,
|
||||
lastGeoURI: .init(latitude: 41.902782, longitude: 12.496366))))
|
||||
|
||||
// Replying to a live location
|
||||
LiveLocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
LiveLocationRoomTimelineView(currentDate: .mockToday410,
|
||||
timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
@@ -220,7 +293,8 @@ struct LiveLocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
eventContent: .liveLocation))))
|
||||
|
||||
// Replying to a live location when the content is not live
|
||||
LiveLocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
LiveLocationRoomTimelineView(currentDate: .mockToday410,
|
||||
timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43d866593057761090bb2b96e39ed678833441aa870bac82ac269274a5d78f87
|
||||
size 2062660
|
||||
oid sha256:65ab004d023cb309220f9df6e9dc3126cfb31d252b4d7d6aba543ce2c10ad206
|
||||
size 2183435
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:17a6b41858d52fce6ce52b4d7708a95b70e71ac6ea054a02baac31d3ba6ff9d9
|
||||
size 2088606
|
||||
oid sha256:be432c88671b1c907955e49c1b3bdd0ec8586bfd51d03950d4772588f4c671ed
|
||||
size 2211947
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e08dbc6a8b42d40bf6ef8ef2fbfc9eea5f27934203543b2ea26f9f574dfbb759
|
||||
size 892677
|
||||
oid sha256:2b380179dcc7e86b16651dcae880071124088604f89a58ba69cde77f16deb641
|
||||
size 947975
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e984b80f719db3626eca2185eb0cde375079c92ef4f52a8faf259df7e86a42d0
|
||||
size 919828
|
||||
oid sha256:ae4b809327dac37e277c55a989b1f97aa3cd3efd78b1782ee0f68c4058dffc48
|
||||
size 979991
|
||||
|
||||
Reference in New Issue
Block a user