mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +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:
@@ -88,7 +88,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
|
||||
let openRoomSpan = analyticsService.signpost.addSpan(.timelineLoad, toTransaction: .openRoom)
|
||||
timeline = try await TimelineProxy(timeline: room.timelineWithConfiguration(configuration: .init(focus: .live(hideThreadedEvents: appSettings.threadsEnabled),
|
||||
filter: .eventFilter(filter: excludedEventsFilter),
|
||||
filter: .eventFilter(filter: Self.excludedEventsFilter(appSettings: appSettings)),
|
||||
internalIdPrefix: nil,
|
||||
dateDividerMode: .daily,
|
||||
trackReadReceipts: .messageLikeEvents,
|
||||
@@ -792,7 +792,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
private let excludedEventsFilter: TimelineEventFilter = {
|
||||
private static func excludedEventsFilter(appSettings: AppSettings) -> TimelineEventFilter {
|
||||
var stateEventFilters: [StateEventType] = [.roomAliases,
|
||||
.roomCanonicalAlias,
|
||||
.roomGuestAccess,
|
||||
@@ -808,6 +808,10 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
||||
.policyRuleServer,
|
||||
.policyRuleUser]
|
||||
|
||||
if !appSettings.liveLocationSharingEnabled {
|
||||
stateEventFilters.append(.beaconInfo)
|
||||
}
|
||||
|
||||
return .excludeEventTypes(eventTypes: stateEventFilters.map { FilterTimelineEventType.state(eventType: $0) })
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,10 @@ struct RoomEventStringBuilder {
|
||||
default: L10n.commonWaitingForDecryptionKey
|
||||
}
|
||||
return prefix(errorMessage, with: displayName, isOutgoing: isOutgoing)
|
||||
case .liveLocation:
|
||||
return messageEventStringBuilder.buildAttributedStringForLiveLocation(senderDisplayName: displayName, isOutgoing: isOutgoing)
|
||||
case .other:
|
||||
return nil // We shouldn't receive these without asking for custom event types.
|
||||
case .liveLocation:
|
||||
// TODO: Implement
|
||||
return nil
|
||||
}
|
||||
case .failedToParseMessageLike, .failedToParseState:
|
||||
return prefix(L10n.commonUnsupportedEvent, with: displayName, isOutgoing: isOutgoing)
|
||||
|
||||
@@ -78,6 +78,19 @@ struct RoomMessageEventStringBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
func buildAttributedStringForLiveLocation(senderDisplayName: String, isOutgoing: Bool) -> AttributedString {
|
||||
var message = AttributedString(L10n.commonSharedLiveLocation)
|
||||
if destination == .pinnedEvent {
|
||||
message.bold()
|
||||
}
|
||||
|
||||
if destination == .roomList {
|
||||
return prefix(message, with: isOutgoing ? L10n.commonYou : senderDisplayName)
|
||||
} else {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
private func buildMessage(for destination: Destination, caption: String?, type: String) -> AttributedString {
|
||||
guard let caption else {
|
||||
return AttributedString(type)
|
||||
|
||||
@@ -70,7 +70,7 @@ struct GeoURI: Hashable {
|
||||
private typealias RegexGeoURI = Regex<(Substring, latitude: Substring, longitude: Substring, uncertainty: Substring?)>
|
||||
|
||||
private extension RegexGeoURI {
|
||||
static let standard: Self = /geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?/
|
||||
static let standard: Self = /geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?/
|
||||
}
|
||||
|
||||
extension GeoURI {
|
||||
|
||||
@@ -11,5 +11,6 @@ import Foundation
|
||||
enum TimelineEventContent: Hashable {
|
||||
case message(EventBasedMessageTimelineItemContentType)
|
||||
case poll(question: String)
|
||||
case liveLocation
|
||||
case redacted
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// 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 Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct LiveLocationRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
|
||||
let id: TimelineItemIdentifier
|
||||
/// Always empty just here for protocol conformance
|
||||
var body: String {
|
||||
""
|
||||
}
|
||||
|
||||
let timestamp: Date
|
||||
let isOutgoing: Bool
|
||||
let isEditable: Bool
|
||||
let canBeRepliedTo: Bool
|
||||
|
||||
let sender: TimelineItemSender
|
||||
let content: LiveLocationRoomTimelineItemContent
|
||||
var properties = RoomTimelineItemProperties()
|
||||
}
|
||||
|
||||
struct LiveLocationRoomTimelineItemContent: Equatable {
|
||||
let isLive: Bool
|
||||
let timeoutDate: Date
|
||||
|
||||
let lastGeoURI: GeoURI?
|
||||
}
|
||||
|
||||
extension LiveLocationRoomTimelineItemContent {
|
||||
init(from content: MatrixRustSDK.LiveLocationContent, timestamp: Date) {
|
||||
isLive = content.isLive
|
||||
timeoutDate = timestamp.addingTimeInterval(Double(content.timeoutMs) / 1000)
|
||||
lastGeoURI = content.locations.last.flatMap { GeoURI(string: $0.geoUri) }
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
return buildRedactedTimelineItem(eventItemProxy, messageLikeContent, isOutgoing)
|
||||
case .unableToDecrypt(let encryptedMessage):
|
||||
return buildEncryptedTimelineItem(eventItemProxy, messageLikeContent, encryptedMessage, isOutgoing)
|
||||
case .liveLocation(let content):
|
||||
return buildLiveLocationTimelineItem(eventItemProxy, messageLikeContent, content, isOutgoing)
|
||||
case .other:
|
||||
return nil // We shouldn't receive these without asking for custom event types.
|
||||
case .liveLocation:
|
||||
// TODO: Implement
|
||||
return nil
|
||||
}
|
||||
case .failedToParseMessageLike(let eventType, let error):
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing)
|
||||
@@ -433,6 +432,27 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
encryptionForwarder: eventItemProxy.forwarder))
|
||||
}
|
||||
|
||||
private func buildLiveLocationTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ messageLikeContent: MsgLikeContent,
|
||||
_ liveLocationContent: LiveLocationContent,
|
||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||
LiveLocationRoomTimelineItem(id: eventItemProxy.id,
|
||||
timestamp: eventItemProxy.timestamp,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
||||
sender: eventItemProxy.sender,
|
||||
content: .init(from: liveLocationContent, timestamp: eventItemProxy.timestamp),
|
||||
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||
isThreaded: messageLikeContent.threadRoot != nil,
|
||||
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
|
||||
encryptionAuthenticity: buildEncryptionAuthenticity(eventItemProxy.shieldState),
|
||||
encryptionForwarder: eventItemProxy.forwarder))
|
||||
}
|
||||
|
||||
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ messageLikeContent: MsgLikeContent,
|
||||
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||
@@ -850,6 +870,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
replyContent = .message(.text(.init(body: body)))
|
||||
case .redacted:
|
||||
replyContent = .redacted
|
||||
case .liveLocation:
|
||||
replyContent = .liveLocation
|
||||
default:
|
||||
replyContent = .message(.text(.init(body: L10n.commonUnsupportedEvent)))
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ struct RoomTimelineItemView: View {
|
||||
CallInviteRoomTimelineView(timelineItem: item)
|
||||
case .callNotification(let item):
|
||||
CallNotificationRoomTimelineView(timelineItem: item)
|
||||
case .liveLocation(let item):
|
||||
LiveLocationRoomTimelineView(timelineItem: item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ enum RoomTimelineItemType: Equatable {
|
||||
case voice(VoiceMessageRoomTimelineItem)
|
||||
case callInvite(CallInviteRoomTimelineItem)
|
||||
case callNotification(CallNotificationRoomTimelineItem)
|
||||
case liveLocation(LiveLocationRoomTimelineItem)
|
||||
|
||||
init(item: RoomTimelineItemProtocol) {
|
||||
switch item {
|
||||
@@ -111,6 +112,8 @@ enum RoomTimelineItemType: Equatable {
|
||||
self = .callInvite(item)
|
||||
case let item as CallNotificationRoomTimelineItem:
|
||||
self = .callNotification(item)
|
||||
case let item as LiveLocationRoomTimelineItem:
|
||||
self = .liveLocation(item)
|
||||
default:
|
||||
fatalError("Unknown timeline item")
|
||||
}
|
||||
@@ -139,7 +142,8 @@ enum RoomTimelineItemType: Equatable {
|
||||
.poll(let item as RoomTimelineItemProtocol),
|
||||
.voice(let item as RoomTimelineItemProtocol),
|
||||
.callInvite(let item as RoomTimelineItemProtocol),
|
||||
.callNotification(let item as RoomTimelineItemProtocol):
|
||||
.callNotification(let item as RoomTimelineItemProtocol),
|
||||
.liveLocation(let item as RoomTimelineItemProtocol):
|
||||
return item.id
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user