Update how file captions are rendered (#3554)

* Update the File/Audio captions layout to match Figma.

* Fix caption sizing issues.

The send info label was being added incorrectly.

* Update icon size and regenerate snapshots.

* Fix a regression in the location timeline item layout.
This commit is contained in:
Doug 2024-11-26 10:36:46 +00:00 committed by GitHub
parent ba8edc31c7
commit 03aaf849ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 191 additions and 113 deletions

View File

@ -8315,7 +8315,7 @@
repositoryURL = "https://github.com/element-hq/compound-ios";
requirement = {
kind = revision;
revision = f0436aa767f614584bf119defa6372ddb3b60080;
revision = 901f3f2fc150db82cf8a2c4da53914b31f681b56;
};
};
F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */ = {

View File

@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/compound-design-tokens",
"state" : {
"revision" : "3adf924aec63f1addfe9124a95fa4c9e9b5bff7d",
"version" : "2.1.0"
"revision" : "31b236f02c811704b68e8aae429865fe8eb8d8ba",
"version" : "2.1.1"
}
},
{
@ -15,7 +15,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/compound-ios",
"state" : {
"revision" : "f0436aa767f614584bf119defa6372ddb3b60080"
"revision" : "901f3f2fc150db82cf8a2c4da53914b31f681b56"
}
},
{

View File

@ -237,6 +237,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
duration: 100,
waveform: nil,
source: nil,
fileSize: nil,
contentType: nil)))),
.loaded(sender: .init(id: "James"),
eventID: "123",
@ -246,6 +247,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
eventContent: .message(.file(.init(filename: "brain-surgery.pdf",
caption: "File: Crash course in brain surgery",
source: nil,
fileSize: nil,
thumbnailSource: nil,
contentType: nil)))),
.loaded(sender: .init(id: "Cliff"),

View File

@ -250,6 +250,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
duration: 0,
waveform: nil,
source: nil,
fileSize: nil,
contentType: nil))))),
TimelineReplyView(placement: .timeline,
@ -258,6 +259,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
eventContent: .message(.file(.init(filename: "file.txt",
caption: "Some file",
source: nil,
fileSize: nil,
thumbnailSource: nil,
contentType: nil))))),
@ -289,6 +291,7 @@ struct TimelineReplyView_Previews: PreviewProvider, TestablePreview {
duration: 0,
waveform: nil,
source: nil,
fileSize: nil,
contentType: nil))))),
TimelineReplyView(placement: .timeline,
timelineItemReplyDetails: .loaded(sender: .init(id: "", displayName: "Bob"),

View File

@ -514,6 +514,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
fileSize: nil,
contentType: nil),
properties: RoomTimelineItemProperties(encryptionAuthenticity: .notGuaranteed(color: .gray))),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
@ -552,6 +553,7 @@ private struct MockTimelineContent: View {
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
fileSize: nil,
contentType: nil),
replyDetails: replyDetails))
@ -565,6 +567,7 @@ private struct MockTimelineContent: View {
content: .init(filename: "file.txt",
caption: "File",
source: nil,
fileSize: nil,
thumbnailSource: nil,
contentType: nil),
replyDetails: replyDetails))
@ -616,6 +619,7 @@ private struct MockTimelineContent: View {
duration: 100,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
fileSize: nil,
contentType: nil),
replyDetails: replyDetails),
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),

View File

@ -152,12 +152,20 @@ private extension TimelineItemSendInfo {
layoutType = switch timelineItem {
case is TextBasedRoomTimelineItem:
.overlay(capsuleStyle: false)
case let message as EventBasedMessageTimelineItemProtocol where message is ImageRoomTimelineItem || message is VideoRoomTimelineItem:
.overlay(capsuleStyle: !message.hasMediaCaption)
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 let locationTimelineItem as LocationRoomTimelineItem:
.overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil)
case is PollRoomTimelineItem:
.vertical(spacing: 16)
default:

View File

@ -10,18 +10,16 @@ import SwiftUI
struct AudioRoomTimelineView: View {
let timelineItem: AudioRoomTimelineItem
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
Label(title: { Text(timelineItem.body) },
icon: { Image(systemName: "waveform")
.foregroundColor(.compound.iconPrimary)
})
.labelStyle(RoomTimelineViewLabelStyle())
.font(.compound.bodyLG)
.padding(.vertical, 12)
.padding(.horizontal, 6)
.accessibilityLabel(L10n.commonAudio)
MediaFileRoomTimelineContent(filename: timelineItem.content.filename,
fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption,
additionalWhitespaces: timelineItem.additionalWhitespaces(),
isAudioFile: true)
.accessibilityLabel(L10n.commonAudio)
}
}
}
@ -30,21 +28,31 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock
static var previews: some View {
body.environmentObject(viewModel.context)
VStack(spacing: 20) {
AudioRoomTimelineView(timelineItem: makeItem(filename: "audio.ogg",
fileSize: 2 * 1024 * 1024))
AudioRoomTimelineView(timelineItem: makeItem(filename: "Best Song Ever.mp3",
fileSize: 7 * 1024 * 1024,
caption: "This song rocks!"))
}
.environmentObject(viewModel.context)
}
static var body: some View {
AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "audio.ogg",
duration: 300,
waveform: nil,
source: nil,
contentType: nil)))
static func makeItem(filename: String, fileSize: UInt, caption: String? = nil) -> AudioRoomTimelineItem {
.init(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: filename,
caption: caption,
duration: 300,
waveform: nil,
source: nil,
fileSize: fileSize,
contentType: nil))
}
}

View File

@ -13,63 +13,110 @@ struct FileRoomTimelineView: View {
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
Label { Text(timelineItem.body) } icon: {
CompoundIcon(\.document)
.foregroundColor(.compound.iconPrimary)
}
.labelStyle(RoomTimelineViewLabelStyle())
.font(.compound.bodyLG)
.padding(.vertical, 8)
.padding(.horizontal, 6)
.accessibilityLabel(L10n.commonFile)
MediaFileRoomTimelineContent(filename: timelineItem.content.filename,
fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption,
additionalWhitespaces: timelineItem.additionalWhitespaces())
.accessibilityLabel(L10n.commonFile)
}
}
}
// MARK: Content
struct MediaFileRoomTimelineContent: View {
let filename: String
let fileSize: UInt?
let caption: String?
let formattedCaption: AttributedString?
let additionalWhitespaces: Int
var isAudioFile = false
var icon: KeyPath<CompoundIcons, Image> {
isAudioFile ? \.audio : \.attachment
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
filePreview
if let formattedCaption {
FormattedBodyText(attributedString: formattedCaption,
additionalWhitespacesCount: additionalWhitespaces)
} else if let caption {
FormattedBodyText(text: caption,
additionalWhitespacesCount: additionalWhitespaces)
}
}
}
var filePreview: some View {
Label {
HStack(spacing: 4) {
Text(filename)
.truncationMode(.middle)
if let fileSize {
Text("(\(fileSize.formatted(.byteCount(style: .file))))")
.layoutPriority(1) // We want the filename to truncate rather than the size.
}
}
.font(.compound.bodyLG)
.foregroundStyle(.compound.textPrimary)
.lineLimit(1)
} icon: {
CompoundIcon(icon, size: .xSmall, relativeTo: .body)
.foregroundColor(.compound.iconPrimary)
.scaledPadding(8)
.background(.compound.iconOnSolidPrimary, in: Circle())
}
.labelStyle(.custom(spacing: 8, alignment: .center))
.padding(.horizontal, 4) // Add to the styler's padding of 8, as we use the default insets for the caption.
}
}
// MARK: - Previews
struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock
static var previews: some View {
body.environmentObject(viewModel.context)
VStack(spacing: 20.0) {
FileRoomTimelineView(timelineItem: makeItem(filename: "document.pdf"))
FileRoomTimelineView(timelineItem: makeItem(filename: "document.pdf",
fileSize: 3 * 1024 * 1024))
FileRoomTimelineView(timelineItem: makeItem(filename: "spreadsheet.xlsx",
fileSize: 17 * 1024,
caption: "The important figures you asked me to send over."))
FileRoomTimelineView(timelineItem: makeItem(filename: "document.txt",
fileSize: 456,
caption: "Plain caption",
formattedCaption: "Formatted caption"))
}
.environmentObject(viewModel.context)
}
static var body: some View {
VStack(spacing: 20.0) {
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "document.pdf",
source: nil,
thumbnailSource: nil,
contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "document.docx",
source: nil,
thumbnailSource: nil,
contentType: nil)))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "document.txt",
source: nil,
thumbnailSource: nil,
contentType: nil)))
}
static func makeItem(filename: String,
fileSize: UInt? = nil,
caption: String? = nil,
formattedCaption: AttributedString? = nil) -> FileRoomTimelineItem {
.init(id: .randomEvent,
timestamp: "Now",
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: filename,
caption: caption,
formattedCaption: formattedCaption,
source: nil,
fileSize: fileSize,
thumbnailSource: nil,
contentType: nil))
}
}

View File

@ -17,5 +17,6 @@ struct AudioRoomTimelineItemContent: Hashable {
let duration: TimeInterval
let waveform: EstimatedWaveform?
let source: MediaSourceProxy?
let fileSize: UInt?
let contentType: UTType?
}

View File

@ -15,6 +15,7 @@ struct FileRoomTimelineItemContent: Hashable {
/// The original textual representation of the formatted caption directly from the event (usually HTML code)
var formattedCaptionHTMLString: String?
let source: MediaSourceProxy?
let fileSize: UInt?
let thumbnailSource: MediaSourceProxy?
let contentType: UTType?
}

View File

@ -67,6 +67,7 @@ struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
duration: 300,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
fileSize: nil,
contentType: nil))
static let playerState = AudioPlayerState(id: .timelineItemIdentifier(timelineItemIdentifier),

View File

@ -503,6 +503,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
duration: messageContent.audio?.duration ?? 0,
waveform: waveform,
source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype),
fileSize: messageContent.info?.size.map(UInt.init),
contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename))
}
@ -572,6 +573,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
formattedCaption: formattedCaption,
formattedCaptionHTMLString: htmlCaption,
source: MediaSourceProxy(source: messageContent.source, mimeType: messageContent.info?.mimetype),
fileSize: messageContent.info?.size.map(UInt.init),
thumbnailSource: thumbnailSource,
contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename))
}

View File

@ -173,6 +173,7 @@ class LoggingTests: XCTestCase {
content: .init(filename: "FileString",
caption: "FileString",
source: nil,
fileSize: nil,
thumbnailSource: nil,
contentType: nil))

View File

@ -65,7 +65,7 @@ packages:
# path: ../matrix-rust-sdk
Compound:
url: https://github.com/element-hq/compound-ios
revision: f0436aa767f614584bf119defa6372ddb3b60080
revision: 901f3f2fc150db82cf8a2c4da53914b31f681b56
# path: ../compound-ios
AnalyticsEvents:
url: https://github.com/matrix-org/matrix-analytics-events