* Update Send button bg color Change the gradient bg to accent/rest. * Tidy-up ComposerToolbar to match iOS 18 Figma. Also simplifies the tests a bit. * Add a .glassEffect to Compound's SendButton. * Add a border to TimelineReplyView. Also use the same sizes in both the message bubbles and the composer. * Change icon size and container in message bubbles - Container size = 36x36px - Icon size = 24x24px * Update icon of reply contents to be 24x24 * Update the VoiceMessageButton to match the designs. * Adopt Liquid Glass in the ComposerToolbar. * Generate and fix snapshots. --------- Co-authored-by: Doug <douglase@element.io>
150 lines
5.8 KiB
Swift
150 lines
5.8 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-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
|
|
|
|
struct FileRoomTimelineView: View {
|
|
@Environment(\.timelineContext) private var context
|
|
let timelineItem: FileRoomTimelineItem
|
|
|
|
var body: some View {
|
|
TimelineStyler(timelineItem: timelineItem) {
|
|
MediaFileRoomTimelineContent(filename: timelineItem.content.filename,
|
|
fileSize: timelineItem.content.fileSize,
|
|
caption: timelineItem.content.caption,
|
|
formattedCaption: timelineItem.content.formattedCaption,
|
|
additionalWhitespaces: timelineItem.additionalWhitespaces(),
|
|
shouldBoost: timelineItem.shouldBoost) {
|
|
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
|
}
|
|
.accessibilityLabel(L10n.commonFile)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Content
|
|
|
|
struct MediaFileRoomTimelineContent: View {
|
|
let filename: String
|
|
let fileSize: UInt?
|
|
let caption: String?
|
|
let formattedCaption: AttributedString?
|
|
let additionalWhitespaces: Int
|
|
var shouldBoost = false
|
|
var isAudioFile = false
|
|
|
|
private var fileDescription: String {
|
|
var fileDescription = "\(filename.validatedFileExtension.uppercased())"
|
|
if let fileSize {
|
|
fileDescription += " (\(fileSize.formatted(.byteCount(style: .file))))"
|
|
}
|
|
return fileDescription
|
|
}
|
|
|
|
var onMediaTap: (() -> Void)?
|
|
|
|
private var icon: KeyPath<CompoundIcons, Image> {
|
|
isAudioFile ? \.audio : \.attachment
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
if let onMediaTap {
|
|
filePreview
|
|
.onTapGesture {
|
|
onMediaTap()
|
|
}
|
|
} else {
|
|
filePreview
|
|
}
|
|
|
|
if let formattedCaption {
|
|
FormattedBodyText(attributedString: formattedCaption,
|
|
additionalWhitespacesCount: additionalWhitespaces,
|
|
boostFontSize: shouldBoost)
|
|
} else if let caption {
|
|
FormattedBodyText(text: caption,
|
|
additionalWhitespacesCount: additionalWhitespaces,
|
|
boostFontSize: shouldBoost)
|
|
}
|
|
}
|
|
}
|
|
|
|
var filePreview: some View {
|
|
Label {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(filename)
|
|
.foregroundStyle(.compound.textPrimary)
|
|
.font(.compound.bodyLG)
|
|
Text(fileDescription)
|
|
.font(.compound.bodySM)
|
|
.foregroundStyle(.compound.textSecondary)
|
|
}
|
|
.font(.compound.bodyLG)
|
|
.foregroundStyle(.compound.textPrimary)
|
|
.lineLimit(2)
|
|
} icon: {
|
|
CompoundIcon(icon, size: .medium, relativeTo: .body)
|
|
.foregroundColor(.compound.iconPrimary)
|
|
.scaledPadding(6)
|
|
.background(.compound.iconOnSolidPrimary,
|
|
in: RoundedRectangle(cornerRadius: 4, style: .continuous))
|
|
}
|
|
.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 {
|
|
VStack(spacing: 20.0) {
|
|
FileRoomTimelineView(timelineItem: makeItem(filename: "document.pdf"))
|
|
|
|
FileRoomTimelineView(timelineItem: makeItem(filename: "document.pdf",
|
|
fileSize: 3 * 1024 * 1024))
|
|
|
|
FileRoomTimelineView(timelineItem: makeItem(filename: "very very very very long named 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 func makeItem(filename: String,
|
|
fileSize: UInt? = nil,
|
|
caption: String? = nil,
|
|
formattedCaption: AttributedString? = nil) -> FileRoomTimelineItem {
|
|
.init(id: .randomEvent,
|
|
timestamp: .mock,
|
|
isOutgoing: false,
|
|
isEditable: false,
|
|
canBeRepliedTo: true,
|
|
sender: .init(id: "Bob"),
|
|
content: .init(filename: filename,
|
|
caption: caption,
|
|
formattedCaption: formattedCaption,
|
|
source: nil,
|
|
fileSize: fileSize,
|
|
thumbnailSource: nil,
|
|
contentType: nil))
|
|
}
|
|
}
|