Skip to content

Commit 64052ee

Browse files
committed
render the LLS timeline item as the static one for testing purposes (for now)
1 parent ddd505c commit 64052ee

File tree

11 files changed

+145
-24
lines changed

11 files changed

+145
-24
lines changed

AccessibilityTests/Sources/GeneratedAccessibilityTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ extension AccessibilityTests {
291291
try await performAccessibilityAudit(named: "LinkNewDeviceScreen_Previews")
292292
}
293293

294+
func testLiveLocationRoomTimelineView() async throws {
295+
try await performAccessibilityAudit(named: "LiveLocationRoomTimelineView_Previews")
296+
}
297+
294298
func testLoadableImage() async throws {
295299
try await performAccessibilityAudit(named: "LoadableImage_Previews")
296300
}

ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ enum TestablePreviewsDictionary {
8080
"LeaveSpaceView_Previews" : LeaveSpaceView_Previews.self,
8181
"LegalInformationScreen_Previews" : LegalInformationScreen_Previews.self,
8282
"LinkNewDeviceScreen_Previews" : LinkNewDeviceScreen_Previews.self,
83+
"LiveLocationRoomTimelineView_Previews" : LiveLocationRoomTimelineView_Previews.self,
8384
"LoadableImage_Previews" : LoadableImage_Previews.self,
8485
"LocationMarkerView_Previews" : LocationMarkerView_Previews.self,
8586
"LocationPickerSheet_Previews" : LocationPickerSheet_Previews.self,

ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,14 +271,18 @@ private extension EventBasedTimelineItemProtocol {
271271
return locationTimelineItem.content.geoURI == nil ||
272272
properties.replyDetails != nil ||
273273
properties.isThreaded ? defaultInsets : .zero
274+
case let liveLocationTimelineItem as LiveLocationRoomTimelineItem:
275+
return liveLocationTimelineItem.content.lastLocation?.geoURI == nil ||
276+
properties.replyDetails != nil ||
277+
properties.isThreaded ? defaultInsets : .zero
274278
default:
275279
return defaultInsets
276280
}
277281
}
278282

279283
var contentCornerRadius: CGFloat {
280284
switch self {
281-
case is ImageRoomTimelineItem, is VideoRoomTimelineItem, is LocationRoomTimelineItem:
285+
case is ImageRoomTimelineItem, is VideoRoomTimelineItem, is LocationRoomTimelineItem, is LiveLocationRoomTimelineItem:
282286
return properties.replyDetails != nil || properties.isThreaded ? 8 : .zero
283287
default:
284288
return .zero

ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ private extension TimelineItemSendInfo {
164164
layoutType = switch timelineItem {
165165
case is TextBasedRoomTimelineItem:
166166
.overlay(capsuleStyle: false)
167+
case let liveLocationTimelineItem as LiveLocationRoomTimelineItem:
168+
.overlay(capsuleStyle: liveLocationTimelineItem.content.lastLocation?.geoURI != nil)
167169
case let message as EventBasedMessageTimelineItemProtocol:
168170
switch message {
169171
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//
2+
// Copyright 2026 Element Creations Ltd.
3+
//
4+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
5+
// Please see LICENSE files in the repository root for full details.
6+
//
7+
8+
import SwiftUI
9+
10+
struct LiveLocationRoomTimelineView: View {
11+
@Environment(\.timelineContext) private var context: TimelineViewModel.Context!
12+
let timelineItem: LiveLocationRoomTimelineItem
13+
14+
var body: some View {
15+
TimelineStyler(timelineItem: timelineItem) {
16+
mainContent
17+
.accessibilityElement(children: .ignore)
18+
.accessibilityLabel(L10n.commonSharedLocation)
19+
.onTapGesture {
20+
guard context.viewState.mapTilerConfiguration.isEnabled else { return }
21+
context.send(viewAction: .mediaTapped(itemID: timelineItem.id))
22+
}
23+
}
24+
}
25+
26+
@ViewBuilder
27+
private var mainContent: some View {
28+
if let geoURI = timelineItem.content.lastLocation?.geoURI {
29+
MapLibreStaticMapView(geoURI: geoURI,
30+
mapURLBuilder: context.viewState.mapTilerConfiguration,
31+
mapSize: .init(width: mapAspectRatio * mapMaxHeight, height: mapMaxHeight)) {
32+
LocationMarkerView(userProfile: .init(sender: timelineItem.sender),
33+
mediaProvider: context.mediaProvider)
34+
}
35+
.frame(maxHeight: mapMaxHeight)
36+
.aspectRatio(mapAspectRatio, contentMode: .fit)
37+
.clipped()
38+
} else {
39+
// The live location is likely loading
40+
FormattedBodyText(text: timelineItem.body, additionalWhitespacesCount: timelineItem.additionalWhitespaces())
41+
}
42+
}
43+
44+
// MARK: - Private
45+
46+
private let mapAspectRatio: Double = 3 / 2
47+
private let mapMaxHeight: Double = 300
48+
}
49+
50+
private extension MapLibreStaticMapView {
51+
init(geoURI: GeoURI, mapURLBuilder: MapTilerURLBuilderProtocol, mapSize: CGSize, @ViewBuilder pinAnnotationView: () -> PinAnnotation) {
52+
self.init(coordinates: .init(latitude: geoURI.latitude, longitude: geoURI.longitude),
53+
zoomLevel: 15,
54+
attributionPlacement: .bottomLeft,
55+
mapURLBuilder: mapURLBuilder,
56+
mapSize: mapSize,
57+
pinAnnotationView: pinAnnotationView)
58+
}
59+
}
60+
61+
struct LiveLocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
62+
static let viewModel = TimelineViewModel.mock
63+
64+
static var previews: some View {
65+
ScrollView {
66+
VStack(spacing: 8) {
67+
states
68+
}
69+
}
70+
.environmentObject(viewModel.context)
71+
.environment(\.timelineContext, viewModel.context)
72+
.previewDisplayName("Bubbles")
73+
}
74+
75+
@ViewBuilder
76+
static var states: some View {
77+
// No location yet (beacon not yet received)
78+
LiveLocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
79+
timestamp: .mock,
80+
isOutgoing: false,
81+
isEditable: false,
82+
canBeRepliedTo: true,
83+
sender: .init(id: "@bob:matrix.org", displayName: "Bob"),
84+
content: .init(isLive: true,
85+
timeoutDate: .mock,
86+
lastLocation: nil)))
87+
88+
// With a known location
89+
LiveLocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
90+
timestamp: .mock,
91+
isOutgoing: false,
92+
isEditable: false,
93+
canBeRepliedTo: true,
94+
sender: .init(id: "@bob:matrix.org", displayName: "Bob", avatarURL: .mockMXCUserAvatar),
95+
content: .init(isLive: true,
96+
timeoutDate: .mock,
97+
lastLocation: .init(timestamp: .mock,
98+
geoURI: .init(latitude: 41.902782, longitude: 12.496366)))))
99+
}
100+
}

ElementX/Sources/Services/Timeline/GeoURI.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ struct GeoURI: Hashable {
7070
private typealias RegexGeoURI = Regex<(Substring, latitude: Substring, longitude: Substring, uncertainty: Substring?)>
7171

7272
private extension RegexGeoURI {
73-
static let standard: Self = /geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?/
73+
static let standard: Self = /geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?/
7474
}
7575

7676
extension GeoURI {

ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/LiveLocationTimelineItem.swift renamed to ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LiveLocationRoomTimelineItem.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import Foundation
99
import MatrixRustSDK
1010

11-
struct LiveLocationTimelineItem: EventBasedTimelineItemProtocol, Equatable {
11+
struct LiveLocationRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
1212
let id: TimelineItemIdentifier
1313
/// Always empty just here for protocol conformance
1414
var body: String {
@@ -21,18 +21,18 @@ struct LiveLocationTimelineItem: EventBasedTimelineItemProtocol, Equatable {
2121
let canBeRepliedTo: Bool
2222

2323
let sender: TimelineItemSender
24-
let content: LiveLocationTimelineItemContent
24+
let content: LiveLocationRoomTimelineItemContent
2525
var properties = RoomTimelineItemProperties()
2626
}
2727

28-
struct LiveLocationTimelineItemContent: Equatable {
28+
struct LiveLocationRoomTimelineItemContent: Equatable {
2929
let isLive: Bool
3030
let timeoutDate: Date
3131

3232
let lastLocation: BeaconInfo?
3333
}
3434

35-
extension LiveLocationTimelineItemContent {
35+
extension LiveLocationRoomTimelineItemContent {
3636
init(from content: MatrixRustSDK.LiveLocationContent, timestamp: Date) {
3737
isLive = content.isLive
3838
timeoutDate = timestamp.addingTimeInterval(Double(content.timeoutMs) / 1000)

ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -436,22 +436,22 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
436436
_ messageLikeContent: MsgLikeContent,
437437
_ liveLocationContent: LiveLocationContent,
438438
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
439-
LiveLocationTimelineItem(id: eventItemProxy.id,
440-
timestamp: eventItemProxy.timestamp,
441-
isOutgoing: isOutgoing,
442-
isEditable: eventItemProxy.isEditable,
443-
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
444-
sender: eventItemProxy.sender,
445-
content: .init(from: liveLocationContent, timestamp: eventItemProxy.timestamp),
446-
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
447-
isThreaded: messageLikeContent.threadRoot != nil,
448-
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
449-
isEdited: false, // Can never be edited
450-
reactions: buildAggregatedReactions(messageLikeContent.reactions),
451-
deliveryStatus: eventItemProxy.deliveryStatus,
452-
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
453-
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState),
454-
encryptionForwarder: eventItemProxy.forwarder))
439+
LiveLocationRoomTimelineItem(id: eventItemProxy.id,
440+
timestamp: eventItemProxy.timestamp,
441+
isOutgoing: isOutgoing,
442+
isEditable: eventItemProxy.isEditable,
443+
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
444+
sender: eventItemProxy.sender,
445+
content: .init(from: liveLocationContent, timestamp: eventItemProxy.timestamp),
446+
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
447+
isThreaded: messageLikeContent.threadRoot != nil,
448+
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
449+
isEdited: false, // Can never be edited
450+
reactions: buildAggregatedReactions(messageLikeContent.reactions),
451+
deliveryStatus: eventItemProxy.deliveryStatus,
452+
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
453+
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState),
454+
encryptionForwarder: eventItemProxy.forwarder))
455455
}
456456

457457
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,

ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ struct RoomTimelineItemView: View {
7575
CallInviteRoomTimelineView(timelineItem: item)
7676
case .callNotification(let item):
7777
CallNotificationRoomTimelineView(timelineItem: item)
78+
case .liveLocation(let item):
79+
LiveLocationRoomTimelineView(timelineItem: item)
7880
}
7981
}
8082

ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ enum RoomTimelineItemType: Equatable {
6464
case voice(VoiceMessageRoomTimelineItem)
6565
case callInvite(CallInviteRoomTimelineItem)
6666
case callNotification(CallNotificationRoomTimelineItem)
67-
case liveLocation(LiveLocationTimelineItem)
67+
case liveLocation(LiveLocationRoomTimelineItem)
6868

6969
init(item: RoomTimelineItemProtocol) {
7070
switch item {
@@ -112,7 +112,7 @@ enum RoomTimelineItemType: Equatable {
112112
self = .callInvite(item)
113113
case let item as CallNotificationRoomTimelineItem:
114114
self = .callNotification(item)
115-
case let item as LiveLocationTimelineItem:
115+
case let item as LiveLocationRoomTimelineItem:
116116
self = .liveLocation(item)
117117
default:
118118
fatalError("Unknown timeline item")

0 commit comments

Comments
 (0)