mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Add Encryption Authenticity explanations. (#3116)
This commit is contained in:
@@ -29,7 +29,7 @@ struct TimelineItemMenu: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
header
|
||||
messagePreview
|
||||
.frame(idealWidth: 300.0)
|
||||
|
||||
Divider()
|
||||
@@ -63,34 +63,44 @@ struct TimelineItemMenu: View {
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(alignment: .top, spacing: 0.0) {
|
||||
LoadableAvatarImage(url: item.sender.avatarURL,
|
||||
name: item.sender.displayName,
|
||||
contentID: item.sender.id,
|
||||
avatarSize: .user(on: .timeline),
|
||||
imageProvider: context.imageProvider)
|
||||
|
||||
Spacer(minLength: 8.0)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(item.sender.displayName ?? item.sender.id)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.textSelection(.enabled)
|
||||
private var messagePreview: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
HStack(alignment: .top, spacing: 0.0) {
|
||||
LoadableAvatarImage(url: item.sender.avatarURL,
|
||||
name: item.sender.displayName,
|
||||
contentID: item.sender.id,
|
||||
avatarSize: .user(on: .timeline),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(item.timelineMenuDescription)
|
||||
.font(.compound.bodyMD)
|
||||
Spacer(minLength: 8.0)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(item.sender.displayName ?? item.sender.id)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.textSelection(.enabled)
|
||||
|
||||
Text(item.timelineMenuDescription)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Spacer(minLength: 16.0)
|
||||
|
||||
Text(item.timestamp)
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.accessibilityElement(children: .combine)
|
||||
|
||||
Spacer(minLength: 16.0)
|
||||
|
||||
Text(item.timestamp)
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
if let authenticity = item.properties.encryptionAuthenticity {
|
||||
Label(authenticity.message, icon: authenticity.icon, iconSize: .small, relativeTo: .compound.bodySMSemibold)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundStyle(authenticity.foregroundStyle)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 32.0)
|
||||
@@ -169,23 +179,54 @@ struct TimelineItemMenu: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
|
||||
static var previews: some View {
|
||||
testView
|
||||
.previewDisplayName("With button shapes off")
|
||||
testView
|
||||
.environment(\._accessibilityShowButtonShapes, true)
|
||||
.previewDisplayName("With button shapes on")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static var testView: some View {
|
||||
if let item = RoomTimelineItemFixtures.singleMessageChunk.first as? EventBasedTimelineItemProtocol,
|
||||
let actions = TimelineItemMenuActions(isReactable: true, actions: [.copy, .edit, .reply(isThread: false), .pin, .redact], debugActions: [.viewSource]) {
|
||||
TimelineItemMenu(item: item, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
private extension EncryptionAuthenticity {
|
||||
var foregroundStyle: SwiftUI.Color {
|
||||
switch color {
|
||||
case .red: .compound.textCriticalPrimary
|
||||
case .gray: .compound.textSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = RoomScreenViewModel.mock
|
||||
static let (item, actions) = makeItem()
|
||||
static let (backupItem, _) = makeItem(authenticity: .notGuaranteed(color: .gray))
|
||||
static let (unencryptedItem, _) = makeItem(authenticity: .sentInClear(color: .red))
|
||||
|
||||
static var previews: some View {
|
||||
TimelineItemMenu(item: item, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("With button shapes off")
|
||||
|
||||
TimelineItemMenu(item: item, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.environment(\._accessibilityShowButtonShapes, true)
|
||||
.previewDisplayName("With button shapes on")
|
||||
|
||||
TimelineItemMenu(item: backupItem, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Authenticity not guaranteed")
|
||||
|
||||
TimelineItemMenu(item: unencryptedItem, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Unencrypted")
|
||||
}
|
||||
|
||||
static func makeItem(authenticity: EncryptionAuthenticity? = nil) -> (TextRoomTimelineItem, TimelineItemMenuActions)! {
|
||||
guard var item = RoomTimelineItemFixtures.singleMessageChunk.first as? TextRoomTimelineItem,
|
||||
let actions = TimelineItemMenuActions(isReactable: true,
|
||||
actions: [.copy, .edit, .reply(isThread: false), .pin, .redact],
|
||||
debugActions: [.viewSource]) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let authenticity {
|
||||
item.properties.encryptionAuthenticity = authenticity
|
||||
}
|
||||
|
||||
return (item, actions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
|
||||
var messageBubble: some View {
|
||||
contentWithReply
|
||||
.timelineItemSendInfo(timelineItem: timelineItem, adjustedDeliveryStatus: adjustedDeliveryStatus)
|
||||
.timelineItemSendInfo(timelineItem: timelineItem, adjustedDeliveryStatus: adjustedDeliveryStatus, context: context)
|
||||
.bubbleStyle(insets: timelineItem.bubbleInsets,
|
||||
color: timelineItem.bubbleBackgroundColor,
|
||||
corners: roundedCorners)
|
||||
|
||||
@@ -20,15 +20,18 @@ 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?) -> some View {
|
||||
adjustedDeliveryStatus: TimelineItemDeliveryStatus?,
|
||||
context: RoomScreenViewModel.Context) -> some View {
|
||||
modifier(TimelineItemSendInfoModifier(sendInfo: .init(timelineItem: timelineItem,
|
||||
adjustedDeliveryStatus: adjustedDeliveryStatus)))
|
||||
adjustedDeliveryStatus: adjustedDeliveryStatus),
|
||||
context: context))
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the send info to a view with the correct layout.
|
||||
private struct TimelineItemSendInfoModifier: ViewModifier {
|
||||
let sendInfo: TimelineItemSendInfo
|
||||
let context: RoomScreenViewModel.Context
|
||||
|
||||
var layout: AnyLayout {
|
||||
switch sendInfo.layoutType {
|
||||
@@ -44,7 +47,15 @@ private struct TimelineItemSendInfoModifier: ViewModifier {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,26 +66,17 @@ private struct TimelineItemSendInfoLabel: View {
|
||||
|
||||
var statusIcon: KeyPath<CompoundIcons, Image>? {
|
||||
switch sendInfo.status {
|
||||
case .sendingFailed:
|
||||
\.error
|
||||
case .encryptionAuthenticity(.notGuaranteed):
|
||||
\.infoSolid
|
||||
case .encryptionAuthenticity(.unknownDevice),
|
||||
.encryptionAuthenticity(.unsignedDevice),
|
||||
.encryptionAuthenticity(.unverifiedIdentity),
|
||||
.encryptionAuthenticity(.sentInClear):
|
||||
\.lockOff
|
||||
case .none:
|
||||
nil
|
||||
case .sendingFailed: \.error
|
||||
case .encryptionAuthenticity(let authenticity): authenticity.icon
|
||||
case .none: nil
|
||||
}
|
||||
}
|
||||
|
||||
var statusIconAccessibilityLabel: String? {
|
||||
switch sendInfo.status {
|
||||
case .sendingFailed: L10n.commonSendingFailed
|
||||
case .none: nil
|
||||
// Temporary testing strings.
|
||||
case .encryptionAuthenticity(let authenticity): authenticity.message
|
||||
case .none: nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,9 +106,10 @@ private struct TimelineItemSendInfoLabel: View {
|
||||
HStack(spacing: 4) {
|
||||
Text(sendInfo.localizedString)
|
||||
|
||||
if let statusIcon, let statusIconAccessibilityLabel {
|
||||
if let statusIcon {
|
||||
CompoundIcon(statusIcon, size: .xSmall, relativeTo: .compound.bodyXS)
|
||||
.accessibilityLabel(statusIconAccessibilityLabel)
|
||||
.accessibilityLabel(statusIconAccessibilityLabel ?? "")
|
||||
.accessibilityHidden(statusIconAccessibilityLabel == nil)
|
||||
}
|
||||
}
|
||||
.font(.compound.bodyXS)
|
||||
@@ -125,6 +128,7 @@ private struct TimelineItemSendInfo {
|
||||
case overlay(capsuleStyle: Bool)
|
||||
}
|
||||
|
||||
let itemID: TimelineItemIdentifier
|
||||
let localizedString: String
|
||||
var status: Status?
|
||||
let layoutType: LayoutType
|
||||
@@ -143,6 +147,7 @@ private struct TimelineItemSendInfo {
|
||||
|
||||
private extension TimelineItemSendInfo {
|
||||
init(timelineItem: EventBasedTimelineItemProtocol, adjustedDeliveryStatus: TimelineItemDeliveryStatus?) {
|
||||
itemID = timelineItem.id
|
||||
localizedString = timelineItem.localizedSendInfo
|
||||
|
||||
status = if adjustedDeliveryStatus == .sendingFailed {
|
||||
@@ -172,18 +177,9 @@ private extension TimelineItemSendInfo {
|
||||
|
||||
private extension EncryptionAuthenticity {
|
||||
var foregroundStyle: SwiftUI.Color {
|
||||
switch self {
|
||||
case .notGuaranteed(let color),
|
||||
.unknownDevice(let color),
|
||||
.unsignedDevice(let color),
|
||||
.unverifiedIdentity(let color),
|
||||
.sentInClear(let color):
|
||||
switch color {
|
||||
case .red:
|
||||
.compound.textCriticalPrimary
|
||||
case .gray:
|
||||
.compound.textSecondary
|
||||
}
|
||||
switch color {
|
||||
case .red: .compound.textCriticalPrimary
|
||||
case .gray: .compound.textSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,20 +189,25 @@ private extension EncryptionAuthenticity {
|
||||
struct TimelineItemSendInfoLabel_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 16) {
|
||||
TimelineItemSendInfoLabel(sendInfo: .init(localizedString: "09:47 AM",
|
||||
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
|
||||
localizedString: "09:47 AM",
|
||||
layoutType: .horizontal()))
|
||||
TimelineItemSendInfoLabel(sendInfo: .init(localizedString: "09:47 AM",
|
||||
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
|
||||
localizedString: "09:47 AM",
|
||||
status: .sendingFailed,
|
||||
layoutType: .horizontal()))
|
||||
TimelineItemSendInfoLabel(sendInfo: .init(localizedString: "09:47 AM",
|
||||
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
|
||||
localizedString: "09:47 AM",
|
||||
status: .encryptionAuthenticity(.unsignedDevice(color: .red)),
|
||||
layoutType: .horizontal()))
|
||||
TimelineItemSendInfoLabel(sendInfo: .init(localizedString: "09:47 AM",
|
||||
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
|
||||
localizedString: "09:47 AM",
|
||||
status: .encryptionAuthenticity(.notGuaranteed(color: .gray)),
|
||||
layoutType: .horizontal()))
|
||||
// TimelineItemSendInfoLabel(sendInfo: .init(localizedString: "09:47 AM",
|
||||
// status: .unencrypted,
|
||||
// layoutType: .horizontal()))
|
||||
TimelineItemSendInfoLabel(sendInfo: .init(itemID: .random,
|
||||
localizedString: "09:47 AM",
|
||||
status: .encryptionAuthenticity(.sentInClear(color: .red)),
|
||||
layoutType: .horizontal()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user