mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Render the live location timeline item (#5209)
* created the data for the timeline item # Conflicts: # ElementX/Sources/Other/Logging/Tracing.swift # ElementX/Sources/Services/Authentication/AuthenticationService.swift # ElementX/Sources/Services/Authentication/LinkNewDeviceService.swift * Implemented the LLS timeline item * updated preview tests * pr and design suggestions * previewscrollview is debug only
This commit is contained in:
@@ -34,6 +34,25 @@ extension Date {
|
||||
}
|
||||
}
|
||||
|
||||
/// The date of an expiration formatted with the minimal necessary units given how long in the future it is.
|
||||
func formattedExpiration() -> String {
|
||||
let calendar = Calendar.current
|
||||
let now = Date.now
|
||||
|
||||
guard let tomorrow = calendar.date(byAdding: .hour, value: 24, to: now),
|
||||
let oneYearFromNow = calendar.date(byAdding: .year, value: 1, to: now) else {
|
||||
return formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
|
||||
if self < tomorrow {
|
||||
return formatted(date: .omitted, time: .shortened)
|
||||
} else if self < oneYearFromNow {
|
||||
return formatted(.dateTime.day().month())
|
||||
} else {
|
||||
return formatted(.dateTime.year())
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to ``formattedMinimal`` but returning "Today" instead of the time and
|
||||
/// including the year when it the date is from a previous year (rather than over a year ago).
|
||||
func formattedDateSeparator() -> String {
|
||||
@@ -60,6 +79,12 @@ 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
|
||||
|
||||
@@ -34,6 +34,19 @@ struct MapLibreStaticMapView<PinAnnotation: View>: View {
|
||||
self.pinAnnotationView = pinAnnotationView()
|
||||
}
|
||||
|
||||
init(geoURI: GeoURI,
|
||||
mapURLBuilder: MapTilerURLBuilderProtocol,
|
||||
attributionPlacement: MapTilerAttributionPlacement = .bottomLeft,
|
||||
mapSize: CGSize,
|
||||
@ViewBuilder pinAnnotationView: () -> PinAnnotation) {
|
||||
self.init(coordinates: .init(latitude: geoURI.latitude, longitude: geoURI.longitude),
|
||||
zoomLevel: 15,
|
||||
attributionPlacement: attributionPlacement,
|
||||
mapURLBuilder: mapURLBuilder,
|
||||
mapSize: mapSize,
|
||||
pinAnnotationView: pinAnnotationView)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
if let url = mapURLBuilder.staticMapTileImageURL(for: colorScheme.mapStyle,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
#if DEBUG
|
||||
/// Only use in Previews! This useful scroll view allows you to still have a scroll view when previewing in Xcode
|
||||
/// but ignores it when running the tests, which allows you to still use it's content directly in preview tests
|
||||
/// and render the preview with `sizeThatFits` layout.
|
||||
struct PreviewScrollView<Content: View>: View {
|
||||
var content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
if ProcessInfo.isRunningTests {
|
||||
content()
|
||||
} else {
|
||||
ScrollView {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -10,16 +10,10 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct LocationMarkerView: View {
|
||||
var userProfile: UserProfileProxy?
|
||||
var fillColor = Color.compound.bgCanvasDefault
|
||||
var strokeColor = Color.compound.iconSecondaryAlpha
|
||||
var dotColor = Color.compound.iconPrimary
|
||||
var kind: LocationMarkerKind
|
||||
@ScaledMetric var size: CGFloat = 42
|
||||
var mediaProvider: MediaProviderProtocol?
|
||||
|
||||
private let circleCenter = CGPoint(x: 21, y: 21) // in SVG space
|
||||
private let circleRadius: CGFloat = 6 // in SVG space
|
||||
|
||||
var body: some View {
|
||||
// Generated from the SVG
|
||||
Canvas { context, canvasSize in
|
||||
@@ -55,7 +49,7 @@ struct LocationMarkerView: View {
|
||||
p.closeSubpath()
|
||||
}
|
||||
|
||||
context.stroke(pinPath, with: .color(strokeColor), lineWidth: 2 * scaleX)
|
||||
context.stroke(pinPath, with: .color(externalStrokeColor), lineWidth: 2 * scaleX)
|
||||
context.fill(pinPath, with: .color(fillColor))
|
||||
|
||||
// Dot
|
||||
@@ -66,20 +60,20 @@ struct LocationMarkerView: View {
|
||||
context.fill(dotPath, with: .color(dotColor))
|
||||
|
||||
// Draw resolved symbol centered on the circle
|
||||
if userProfile != nil, let symbol = context.resolveSymbol(id: 0) {
|
||||
if kind.userProfile != nil, let symbol = context.resolveSymbol(id: 0) {
|
||||
let center = CGPoint(x: circleCenter.x * scaleX,
|
||||
y: circleCenter.y * scaleY)
|
||||
context.draw(symbol, at: center, anchor: .center)
|
||||
}
|
||||
} symbols: {
|
||||
if let userProfile {
|
||||
if let userProfile = kind.userProfile {
|
||||
LoadableAvatarImage(url: userProfile.avatarURL,
|
||||
name: userProfile.displayName,
|
||||
contentID: userProfile.userID,
|
||||
avatarSize: .user(on: .map),
|
||||
mediaProvider: mediaProvider)
|
||||
.overlay {
|
||||
Circle().inset(by: 0.5).stroke(strokeColor)
|
||||
Circle().inset(by: 0.5).stroke(internalStrokeColor)
|
||||
}
|
||||
.tag(0)
|
||||
}
|
||||
@@ -89,22 +83,77 @@ struct LocationMarkerView: View {
|
||||
dimensions[.bottom]
|
||||
}
|
||||
}
|
||||
|
||||
private let circleCenter = CGPoint(x: 21, y: 21) // in SVG space
|
||||
private let circleRadius: CGFloat = 6 // in SVG space
|
||||
|
||||
private var fillColor: Color {
|
||||
switch kind {
|
||||
case .pin, .staticUser:
|
||||
.compound.bgCanvasDefault
|
||||
case .liveUser:
|
||||
.compound.iconAccentPrimary
|
||||
case .placeholder:
|
||||
.compound.bgSubtleSecondary
|
||||
}
|
||||
}
|
||||
|
||||
private var externalStrokeColor: Color {
|
||||
switch kind {
|
||||
case .pin, .staticUser:
|
||||
.compound.iconSecondaryAlpha
|
||||
case .liveUser:
|
||||
.compound.iconAccentPrimary
|
||||
case .placeholder:
|
||||
.compound.iconDisabled
|
||||
}
|
||||
}
|
||||
|
||||
private var internalStrokeColor: Color {
|
||||
switch kind {
|
||||
case .pin, .staticUser:
|
||||
.compound.iconSecondaryAlpha
|
||||
case .liveUser:
|
||||
.compound.bgCanvasDefault
|
||||
case .placeholder:
|
||||
.compound.iconDisabled
|
||||
}
|
||||
}
|
||||
|
||||
private var dotColor: Color {
|
||||
switch kind {
|
||||
case .placeholder:
|
||||
.compound.iconDisabled
|
||||
default:
|
||||
.compound.iconPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LocationMarkerView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 30) {
|
||||
LocationMarkerView()
|
||||
// Placeholder
|
||||
LocationMarkerView(kind: .placeholder)
|
||||
|
||||
LocationMarkerView()
|
||||
.colorScheme(.dark)
|
||||
// Pin (no user)
|
||||
LocationMarkerView(kind: .pin)
|
||||
|
||||
LocationMarkerView(userProfile: UserProfileProxy.mockDan,
|
||||
// Static user with avatar
|
||||
LocationMarkerView(kind: .staticUser(.mockDan),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||
|
||||
LocationMarkerView(userProfile: UserProfileProxy.mockDan,
|
||||
// Static user without avatar
|
||||
LocationMarkerView(kind: .staticUser(.init(userID: "@someone:matrix.org",
|
||||
displayName: "Someone")))
|
||||
|
||||
// Live user with avatar
|
||||
LocationMarkerView(kind: .liveUser(.mockDan),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||
.colorScheme(.dark)
|
||||
|
||||
// Live user without avatar
|
||||
LocationMarkerView(kind: .liveUser(.init(userID: "@someone:matrix.org",
|
||||
displayName: "Someone")))
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color(red: 0.9, green: 0.85, blue: 0.8))
|
||||
|
||||
@@ -80,6 +80,7 @@ enum TestablePreviewsDictionary {
|
||||
"LeaveSpaceView_Previews" : LeaveSpaceView_Previews.self,
|
||||
"LegalInformationScreen_Previews" : LegalInformationScreen_Previews.self,
|
||||
"LinkNewDeviceScreen_Previews" : LinkNewDeviceScreen_Previews.self,
|
||||
"LiveLocationRoomTimelineView_Previews" : LiveLocationRoomTimelineView_Previews.self,
|
||||
"LoadableImage_Previews" : LoadableImage_Previews.self,
|
||||
"LocationMarkerView_Previews" : LocationMarkerView_Previews.self,
|
||||
"LocationPickerSheet_Previews" : LocationPickerSheet_Previews.self,
|
||||
|
||||
Reference in New Issue
Block a user