Files
letro-ios/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift
Richard van der Hoff bc32a05a2c Promote "history sharing on invite" out of developer options (#5480)
* Enable key-share-on-invite irrespective of feature flag

* Remove feature-flag dep: warning on starting chat with new people

* Remove feature-flag dep: invite from room member details

* Remove feature-flag dep: warning on new users in invite screen

* Remove feature-flag dep: from room details screen

* Remove feature-flag dep: starting chat from user profile screen

* Remove feature-flag dep: timeline info on forwarded keys

* Remove feature-flag dep: RoomScreenModel

* Remove `enableKeyShareOnInvite` from AppSettings

* Remove `enableKeyShareOnInvite` feature flag

* Remove outdated comments

* Update preview test room snapshots as their header now includes the history sharing icon
2026-04-27 14:43:18 +03:00

250 lines
9.6 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2024-2025 New Vector 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
extension View {
/// Adds the send info (timestamp along indicators for edits and delivery/encryption issues) for the given timeline item to this view.
func timelineItemSendInfo(timelineItem: EventBasedTimelineItemProtocol,
adjustedDeliveryStatus: TimelineItemDeliveryStatus?,
context: TimelineViewModel.Context) -> some View {
modifier(TimelineItemSendInfoModifier(sendInfo: .init(timelineItem: timelineItem,
adjustedDeliveryStatus: adjustedDeliveryStatus),
context: context))
}
}
/// Adds the send info to a view with the correct layout.
private struct TimelineItemSendInfoModifier: ViewModifier {
let sendInfo: TimelineItemSendInfo
let context: TimelineViewModel.Context
var layout: AnyLayout {
switch sendInfo.layoutType {
case .horizontal(let spacing):
AnyLayout(HStackLayout(alignment: .bottom, spacing: spacing))
case .vertical(let spacing):
AnyLayout(GridLayout(alignment: .leading, verticalSpacing: spacing))
case .overlay, .hidden:
AnyLayout(ZStackLayout(alignment: .bottomTrailing))
}
}
func body(content: Content) -> some View {
layout {
content
TimelineItemSendInfoLabel(sendInfo: sendInfo)
.contentShape(.rect)
// Tap gesture to avoid the message being detected as a button by VoiceOver
// (and the action shows a description that is already read to the user).
.onTapGesture {
guard sendInfo.status != nil else { return }
context.send(viewAction: .itemSendInfoTapped(itemID: sendInfo.itemID))
}
}
}
}
/// The label shown for a timeline item with info about it's timestamp and various other indicators.
private struct TimelineItemSendInfoLabel: View {
let sendInfo: TimelineItemSendInfo
var statusIcon: KeyPath<CompoundIcons, Image>? {
switch sendInfo.status {
case .sendingFailed: \.errorSolid
case .encryptionAuthenticity(let authenticity): authenticity.icon
case .encryptionForwarder: \.info
case .none: nil
}
}
var statusIconAccessibilityLabel: String? {
switch sendInfo.status {
case .sendingFailed: L10n.commonSendingFailed
case .encryptionAuthenticity(let authenticity): authenticity.message
case .encryptionForwarder(let forwarder): forwarder.message
case .none: nil
}
}
var body: some View {
switch sendInfo.layoutType {
case .overlay(capsuleStyle: true):
content
.padding(.horizontal, 4)
.padding(.vertical, 2)
.background(Color.compound.bgSubtleSecondary)
.cornerRadius(10)
.padding(.trailing, 4)
.padding(.bottom, 4)
case .horizontal, .overlay(capsuleStyle: false):
content
.padding(.bottom, -4)
case .vertical:
GridRow {
content
.gridColumnAlignment(.trailing)
}
case .hidden:
EmptyView()
}
}
var content: some View {
HStack(spacing: 4) {
Text(sendInfo.localizedString)
if let statusIcon {
CompoundIcon(statusIcon, size: .xSmall, relativeTo: .compound.bodyXS)
.accessibilityLabel(statusIconAccessibilityLabel ?? "")
.accessibilityHidden(statusIconAccessibilityLabel == nil)
}
}
.font(.compound.bodyXS)
.foregroundStyle(sendInfo.foregroundStyle)
}
}
/// All the data needed to render a timeline item's send info label.
@MainActor
private struct TimelineItemSendInfo {
enum Status {
case sendingFailed
case encryptionAuthenticity(EncryptionAuthenticity)
case encryptionForwarder(TimelineItemKeyForwarder)
}
/// Describes how the content and the send info should be arranged inside a bubble
enum LayoutType {
case horizontal(spacing: CGFloat = 4)
case vertical(spacing: CGFloat = 4)
case overlay(capsuleStyle: Bool)
case hidden
}
let itemID: TimelineItemIdentifier
let localizedString: String
var status: Status?
let layoutType: LayoutType
var foregroundStyle: Color {
switch status {
case .sendingFailed:
.compound.textCriticalPrimary
case .encryptionAuthenticity(let authenticity):
authenticity.foregroundStyle
case .encryptionForwarder:
.compound.textSecondary
case .none:
.compound.textSecondary
}
}
}
private extension TimelineItemSendInfo {
init(timelineItem: EventBasedTimelineItemProtocol, adjustedDeliveryStatus: TimelineItemDeliveryStatus?) {
itemID = timelineItem.id
localizedString = timelineItem.localizedSendInfo
status = if case .sendingFailed = adjustedDeliveryStatus {
.sendingFailed
} else if let authenticity = timelineItem.properties.encryptionAuthenticity {
.encryptionAuthenticity(authenticity)
} else if let forwarder = timelineItem.properties.encryptionForwarder {
.encryptionForwarder(forwarder)
} else {
nil
}
layoutType = switch timelineItem {
case is TextBasedRoomTimelineItem:
.overlay(capsuleStyle: false)
case let liveLocationTimelineItem as LiveLocationRoomTimelineItem:
liveLocationTimelineItem.layout
case let message as EventBasedMessageTimelineItemProtocol:
switch message {
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
.overlay(capsuleStyle: !message.hasMediaCaption)
case is AudioRoomTimelineItem, is FileRoomTimelineItem:
// swiftlint:disable:next void_function_in_ternary
message.hasMediaCaption ? .overlay(capsuleStyle: false) : .horizontal(spacing: 0) // No spacing as the content already contains it.
case let locationTimelineItem as LocationRoomTimelineItem:
.overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil)
default:
.horizontal()
}
case is StickerRoomTimelineItem:
.overlay(capsuleStyle: true)
case is PollRoomTimelineItem:
.vertical(spacing: 16)
default:
.horizontal()
}
}
}
private extension LiveLocationRoomTimelineItem {
var layout: TimelineItemSendInfo.LayoutType {
if content.isLive, isOutgoing {
.hidden
} else {
.overlay(capsuleStyle: true)
}
}
}
@MainActor
private extension EncryptionAuthenticity {
var foregroundStyle: SwiftUI.Color {
switch color {
case .red: .compound.textCriticalPrimary
case .gray: .compound.textSecondary
}
}
}
private extension TimelineItemKeyForwarder {
static var test: TimelineItemKeyForwarder {
TimelineItemKeyForwarder(id: "@alice:matrix.org", displayName: "alice")
}
}
// MARK: - Previews
struct TimelineItemSendInfoLabel_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 16) {
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
status: .sendingFailed,
layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
status: .encryptionAuthenticity(.unsignedDevice(color: .red)),
layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
status: .encryptionAuthenticity(.notGuaranteed(color: .gray)),
layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
status: .encryptionAuthenticity(.sentInClear(color: .red)),
layoutType: .horizontal()))
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .randomEvent,
localizedString: "09:47 AM",
status: .encryptionForwarder(.test),
layoutType: .horizontal()))
}
}
}