Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom media gallery views for files and voice messages #3610

Merged
merged 3 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 45 additions & 21 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,17 @@ struct MediaEventsTimelineScreen: View {
ForEach(context.viewState.groups) { group in
Section {
ForEach(group.items) { item in
viewForTimelineItem(item)
.scaleEffect(.init(width: 1, height: -1))
.onTapGesture {
VStack(spacing: 20) {
Divider()

Button {
context.send(viewAction: .tappedItem(item))
} label: {
viewForTimelineItem(item)
.scaleEffect(.init(width: 1, height: -1))
}
.accessibilityActions {
Button(L10n.actionShow) {
context.send(viewAction: .tappedItem(item))
}
}
}
.padding(.horizontal, 16)
}
} footer: {
// Use a footer as the header because the scrollView is flipped
Expand Down Expand Up @@ -143,13 +144,13 @@ struct MediaEventsTimelineScreen: View {
case .video(let timelineItem):
VideoMediaEventsTimelineView(timelineItem: timelineItem)
case .file(let timelineItem):
FileRoomTimelineView(timelineItem: timelineItem)
FileMediaEventsTimelineView(timelineItem: timelineItem)
case .audio(let timelineItem):
AudioRoomTimelineView(timelineItem: timelineItem)
AudioMediaEventsTimelineView(timelineItem: timelineItem)
case .voice(let timelineItem):
let defaultPlayerState = AudioPlayerState(id: .timelineItemIdentifier(timelineItem.id), title: L10n.commonVoiceMessage, duration: 0)
let playerState = context.viewState.activeTimelineContextProvider().viewState.audioPlayerStateProvider?(timelineItem.id) ?? defaultPlayerState
VoiceMessageRoomTimelineView(timelineItem: timelineItem, playerState: playerState)
VoiceMessageMediaEventsTimelineView(timelineItem: timelineItem, playerState: playerState)
default:
EmptyView()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//

import Compound
import SwiftUI

struct AudioMediaEventsTimelineView: View {
let timelineItem: AudioRoomTimelineItem

var body: some View {
MediaFileRoomTimelineContent(timelineItemID: timelineItem.id,
stefanceriu marked this conversation as resolved.
Show resolved Hide resolved
filename: timelineItem.content.filename,
fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption,
additionalWhitespaces: timelineItem.additionalWhitespaces(),
isAudioFile: true)
.accessibilityLabel(L10n.commonAudio)
.frame(maxWidth: .infinity, alignment: .leading)
.bubbleBackground(timelineItem: timelineItem,
insets: .init(top: 8, leading: 12, bottom: 8, trailing: 12),
color: .compound.bgSubtleSecondary)
}
}

struct AudioMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock

static var previews: some View {
VStack(spacing: 20) {
AudioMediaEventsTimelineView(timelineItem: makeItem(filename: "audio.ogg",
fileSize: 2 * 1024 * 1024))

AudioMediaEventsTimelineView(timelineItem: makeItem(filename: "Best Song Ever.mp3",
fileSize: 7 * 1024 * 1024,
caption: "This song rocks!"))
}
.environmentObject(viewModel.context)
}

static func makeItem(filename: String, fileSize: UInt, caption: String? = nil) -> AudioRoomTimelineItem {
.init(id: .randomEvent,
timestamp: .mock,
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))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//

import Compound
import SwiftUI

struct FileMediaEventsTimelineView: View {
let timelineItem: FileRoomTimelineItem

var body: some View {
MediaFileRoomTimelineContent(timelineItemID: timelineItem.id,
filename: timelineItem.content.filename,
fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption,
additionalWhitespaces: timelineItem.additionalWhitespaces())
.accessibilityLabel(L10n.commonFile)
.frame(maxWidth: .infinity, alignment: .leading)
.bubbleBackground(timelineItem: timelineItem,
insets: .init(top: 8, leading: 12, bottom: 8, trailing: 12),
color: .compound.bgSubtleSecondary)
}
}

struct FileMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock

static var previews: some View {
VStack(spacing: 20.0) {
FileMediaEventsTimelineView(timelineItem: makeItem(filename: "document.pdf"))

FileMediaEventsTimelineView(timelineItem: makeItem(filename: "document.pdf",
fileSize: 3 * 1024 * 1024))

FileMediaEventsTimelineView(timelineItem: makeItem(filename: "spreadsheet.xlsx",
fileSize: 17 * 1024,
caption: "The important figures you asked me to send over."))

FileMediaEventsTimelineView(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,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: filename,
caption: caption,
formattedCaption: formattedCaption,
source: nil,
fileSize: fileSize,
thumbnailSource: nil,
contentType: nil))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//

import Compound
import SwiftUI

struct VoiceMessageMediaEventsTimelineView: View {
let timelineItem: VoiceMessageRoomTimelineItem
let playerState: AudioPlayerState

var body: some View {
VoiceMessageRoomTimelineContent(timelineItem: timelineItem,
playerState: playerState)
.accessibilityLabel(L10n.commonVoiceMessage)
.frame(maxWidth: .infinity, alignment: .leading)
.bubbleBackground(timelineItem: timelineItem,
insets: .init(top: 8, leading: 12, bottom: 8, trailing: 12),
color: .compound.bgSubtleSecondary)
}
}

// MARK: - Content

struct VoiceMessageMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock
static let timelineItemIdentifier = TimelineItemIdentifier.randomEvent
static let voiceRoomTimelineItem = VoiceMessageRoomTimelineItem(id: timelineItemIdentifier,
timestamp: .mock,
isOutgoing: false,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "Bob"),
content: .init(filename: "audio.ogg",
duration: 300,
waveform: EstimatedWaveform.mockWaveform,
source: nil,
fileSize: nil,
contentType: nil))

static let playerState = AudioPlayerState(id: .timelineItemIdentifier(timelineItemIdentifier),
title: L10n.commonVoiceMessage,
duration: 10.0,
waveform: EstimatedWaveform.mockWaveform,
progress: 0.4)

static var previews: some View {
body
.environmentObject(viewModel.context)
}

static var body: some View {
VoiceMessageMediaEventsTimelineView(timelineItem: voiceRoomTimelineItem, playerState: playerState)
.fixedSize(horizontal: false, vertical: true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//

import SwiftUI

extension View {
func bubbleBackground(timelineItem: EventBasedTimelineItemProtocol,
insets: EdgeInsets,
color: Color? = nil) -> some View {
modifier(TimelineItemBubbleBackgroundModifier(timelineItem: timelineItem,
insets: insets,
color: color))
}
}

private struct TimelineItemBubbleBackgroundModifier: ViewModifier {
@Environment(\.timelineGroupStyle) private var timelineGroupStyle

let timelineItem: EventBasedTimelineItemProtocol
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the perspective of using this for previews, if we passed in the isOutgoing boolean instead of the whole timeline item here, we could use this modifier in places like SwipeRightAction_Previews, LongPressWithFeedback_Previews and FormattedBodyText_Previews and get rid of those made up ones.

let insets: EdgeInsets
var color: Color?

func body(content: Content) -> some View {
content
.padding(insets)
.background(color)
.cornerRadius(12, corners: roundedCorners)
}

private var roundedCorners: UIRectCorner {
switch timelineGroupStyle {
case .single:
return .allCorners
case .first:
if timelineItem.isOutgoing {
return [.topLeft, .topRight, .bottomLeft]
} else {
return [.topLeft, .topRight, .bottomRight]
}
case .middle:
return timelineItem.isOutgoing ? [.topLeft, .bottomLeft] : [.topRight, .bottomRight]
case .last:
if timelineItem.isOutgoing {
return [.topLeft, .bottomLeft, .bottomRight]
} else {
return [.topRight, .bottomLeft, .bottomRight]
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
var messageBubble: some View {
contentWithReply
.timelineItemSendInfo(timelineItem: timelineItem, adjustedDeliveryStatus: adjustedDeliveryStatus, context: context)
.bubbleStyle(insets: timelineItem.bubbleInsets,
color: timelineItem.bubbleBackgroundColor,
corners: roundedCorners)
.bubbleBackground(timelineItem: timelineItem,
insets: timelineItem.bubbleInsets,
color: timelineItem.bubbleBackgroundColor)
}

@ViewBuilder
Expand Down Expand Up @@ -217,40 +217,11 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
timelineItem.isOutgoing ? .trailing : .leading
}

private var roundedCorners: UIRectCorner {
switch timelineGroupStyle {
case .single:
return .allCorners
case .first:
if timelineItem.isOutgoing {
return [.topLeft, .topRight, .bottomLeft]
} else {
return [.topLeft, .topRight, .bottomRight]
}
case .middle:
return timelineItem.isOutgoing ? [.topLeft, .bottomLeft] : [.topRight, .bottomRight]
case .last:
if timelineItem.isOutgoing {
return [.topLeft, .bottomLeft, .bottomRight]
} else {
return [.topRight, .bottomLeft, .bottomRight]
}
}
}

private var shouldShowSenderDetails: Bool {
timelineGroupStyle.shouldShowSenderDetails
}
}

private extension View {
func bubbleStyle(insets: EdgeInsets, color: Color? = nil, cornerRadius: CGFloat = 12, corners: UIRectCorner) -> some View {
padding(insets)
.background(color)
.cornerRadius(cornerRadius, corners: corners)
}
}

private extension EventBasedTimelineItemProtocol {
var bubbleBackgroundColor: Color? {
let defaultColor: Color = isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming
Expand Down
Loading
Loading