Add Encryption Authenticity explanations. (#3116)

This commit is contained in:
Doug
2024-08-06 10:45:46 +01:00
committed by GitHub
parent 6a45ffc939
commit e667be0f43
24 changed files with 224 additions and 103 deletions
@@ -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()))
}
}
}