mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Add support for rendering media captions in the timeline. (#3429)
This commit is contained in:
parent
201e3b0c9c
commit
a0c81cf393
@ -257,7 +257,7 @@ private extension EventBasedTimelineItemProtocol {
|
|||||||
switch self {
|
switch self {
|
||||||
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
|
case is ImageRoomTimelineItem, is VideoRoomTimelineItem:
|
||||||
// In case a reply detail or a thread decorator is present we render the color and the padding
|
// In case a reply detail or a thread decorator is present we render the color and the padding
|
||||||
return self.replyDetails != nil || self.isThreaded ? defaultColor : nil
|
return self.replyDetails != nil || self.isThreaded || self.hasMediaCaption ? defaultColor : nil
|
||||||
default:
|
default:
|
||||||
return defaultColor
|
return defaultColor
|
||||||
}
|
}
|
||||||
@ -283,8 +283,7 @@ private extension EventBasedTimelineItemProtocol {
|
|||||||
// In case a reply detail or a thread decorator is present we render the color and the padding
|
// In case a reply detail or a thread decorator is present we render the color and the padding
|
||||||
case is ImageRoomTimelineItem,
|
case is ImageRoomTimelineItem,
|
||||||
is VideoRoomTimelineItem:
|
is VideoRoomTimelineItem:
|
||||||
return self.replyDetails != nil ||
|
return self.replyDetails != nil || self.isThreaded || self.hasMediaCaption ? defaultInsets : .zero
|
||||||
self.isThreaded ? defaultInsets : .zero
|
|
||||||
case let locationTimelineItem as LocationRoomTimelineItem:
|
case let locationTimelineItem as LocationRoomTimelineItem:
|
||||||
return locationTimelineItem.content.geoURI == nil ||
|
return locationTimelineItem.content.geoURI == nil ||
|
||||||
self.replyDetails != nil ||
|
self.replyDetails != nil ||
|
||||||
|
@ -152,9 +152,9 @@ private extension TimelineItemSendInfo {
|
|||||||
layoutType = switch timelineItem {
|
layoutType = switch timelineItem {
|
||||||
case is TextBasedRoomTimelineItem:
|
case is TextBasedRoomTimelineItem:
|
||||||
.overlay(capsuleStyle: false)
|
.overlay(capsuleStyle: false)
|
||||||
case is ImageRoomTimelineItem,
|
case let message as EventBasedMessageTimelineItemProtocol where message is ImageRoomTimelineItem || message is VideoRoomTimelineItem:
|
||||||
is VideoRoomTimelineItem,
|
.overlay(capsuleStyle: !message.hasMediaCaption)
|
||||||
is StickerRoomTimelineItem:
|
case is StickerRoomTimelineItem:
|
||||||
.overlay(capsuleStyle: true)
|
.overlay(capsuleStyle: true)
|
||||||
case let locationTimelineItem as LocationRoomTimelineItem:
|
case let locationTimelineItem as LocationRoomTimelineItem:
|
||||||
.overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil)
|
.overlay(capsuleStyle: locationTimelineItem.content.geoURI != nil)
|
||||||
|
@ -12,18 +12,35 @@ struct ImageRoomTimelineView: View {
|
|||||||
@EnvironmentObject private var context: TimelineViewModel.Context
|
@EnvironmentObject private var context: TimelineViewModel.Context
|
||||||
let timelineItem: ImageRoomTimelineItem
|
let timelineItem: ImageRoomTimelineItem
|
||||||
|
|
||||||
|
var hasMediaCaption: Bool { timelineItem.content.caption != nil }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
LoadableImage(mediaSource: source,
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
mediaType: .timelineItem,
|
LoadableImage(mediaSource: source,
|
||||||
blurhash: timelineItem.content.blurhash,
|
mediaType: .timelineItem,
|
||||||
mediaProvider: context.mediaProvider) {
|
blurhash: timelineItem.content.blurhash,
|
||||||
placeholder
|
mediaProvider: context.mediaProvider) {
|
||||||
|
placeholder
|
||||||
|
}
|
||||||
|
.timelineMediaFrame(height: timelineItem.content.height,
|
||||||
|
aspectRatio: timelineItem.content.aspectRatio)
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(L10n.commonImage)
|
||||||
|
// This clip shape is distinct from the one in the styler as that one
|
||||||
|
// operates on the entire message so wouldn't round the bottom corners.
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: hasMediaCaption ? 6 : 0))
|
||||||
|
|
||||||
|
if let attributedCaption = timelineItem.content.formattedCaption {
|
||||||
|
FormattedBodyText(attributedString: attributedCaption,
|
||||||
|
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
|
||||||
|
boostEmojiSize: true)
|
||||||
|
} else if let caption = timelineItem.content.caption {
|
||||||
|
FormattedBodyText(text: caption,
|
||||||
|
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
|
||||||
|
boostEmojiSize: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timelineMediaFrame(height: timelineItem.content.height,
|
|
||||||
aspectRatio: timelineItem.content.aspectRatio)
|
|
||||||
.accessibilityElement(children: .ignore)
|
|
||||||
.accessibilityLabel(L10n.commonImage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +104,23 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
aspectRatio: 0.7,
|
aspectRatio: 0.7,
|
||||||
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW",
|
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW",
|
||||||
contentType: .gif)))
|
contentType: .gif)))
|
||||||
|
|
||||||
|
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: .randomEvent,
|
||||||
|
timestamp: "Now",
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
canBeRepliedTo: true,
|
||||||
|
isThreaded: false,
|
||||||
|
sender: .init(id: "Bob"),
|
||||||
|
content: .init(filename: "Blurhashed.jpg",
|
||||||
|
caption: "This is a great image 😎",
|
||||||
|
source: MediaSourceProxy(url: .picturesDirectory, mimeType: "image/png"),
|
||||||
|
thumbnailSource: nil,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
aspectRatio: 1,
|
||||||
|
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW",
|
||||||
|
contentType: .gif)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,30 @@ struct VideoRoomTimelineView: View {
|
|||||||
@EnvironmentObject private var context: TimelineViewModel.Context
|
@EnvironmentObject private var context: TimelineViewModel.Context
|
||||||
let timelineItem: VideoRoomTimelineItem
|
let timelineItem: VideoRoomTimelineItem
|
||||||
|
|
||||||
|
private var hasMediaCaption: Bool { timelineItem.content.caption != nil }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
thumbnail
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
.timelineMediaFrame(height: timelineItem.content.height,
|
thumbnail
|
||||||
aspectRatio: timelineItem.content.aspectRatio)
|
.timelineMediaFrame(height: timelineItem.content.height,
|
||||||
.accessibilityElement(children: .ignore)
|
aspectRatio: timelineItem.content.aspectRatio)
|
||||||
.accessibilityLabel(L10n.commonVideo)
|
.accessibilityElement(children: .ignore)
|
||||||
|
.accessibilityLabel(L10n.commonVideo)
|
||||||
|
// This clip shape is distinct from the one in the styler as that one
|
||||||
|
// operates on the entire message so wouldn't round the bottom corners.
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: hasMediaCaption ? 6 : 0))
|
||||||
|
|
||||||
|
if let attributedCaption = timelineItem.content.formattedCaption {
|
||||||
|
FormattedBodyText(attributedString: attributedCaption,
|
||||||
|
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
|
||||||
|
boostEmojiSize: true)
|
||||||
|
} else if let caption = timelineItem.content.caption {
|
||||||
|
FormattedBodyText(text: caption,
|
||||||
|
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
|
||||||
|
boostEmojiSize: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +117,19 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
thumbnailSource: nil,
|
thumbnailSource: nil,
|
||||||
aspectRatio: 0.7,
|
aspectRatio: 0.7,
|
||||||
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")))
|
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW")))
|
||||||
|
|
||||||
|
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: .randomEvent,
|
||||||
|
timestamp: "Now",
|
||||||
|
isOutgoing: false,
|
||||||
|
isEditable: false,
|
||||||
|
canBeRepliedTo: true,
|
||||||
|
isThreaded: false,
|
||||||
|
sender: .init(id: "Bob"),
|
||||||
|
content: .init(filename: "video.mp4",
|
||||||
|
caption: "This is a caption",
|
||||||
|
duration: 21,
|
||||||
|
source: nil,
|
||||||
|
thumbnailSource: nil)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,3 +24,20 @@ protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol {
|
|||||||
var contentType: EventBasedMessageTimelineItemContentType { get }
|
var contentType: EventBasedMessageTimelineItemContentType { get }
|
||||||
var isThreaded: Bool { get }
|
var isThreaded: Bool { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension EventBasedMessageTimelineItemProtocol {
|
||||||
|
var hasMediaCaption: Bool {
|
||||||
|
switch contentType {
|
||||||
|
case .audio(let content):
|
||||||
|
content.caption != nil
|
||||||
|
case .file(let content):
|
||||||
|
content.caption != nil
|
||||||
|
case .image(let content):
|
||||||
|
content.caption != nil
|
||||||
|
case .video(let content):
|
||||||
|
content.caption != nil
|
||||||
|
case .emote, .notice, .text, .location, .voice:
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_imageRoomTimelineView-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_imageRoomTimelineView-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_imageRoomTimelineView-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_imageRoomTimelineView-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_imageRoomTimelineView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_imageRoomTimelineView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_imageRoomTimelineView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_imageRoomTimelineView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_videoRoomTimelineView-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_videoRoomTimelineView-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_videoRoomTimelineView-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_videoRoomTimelineView-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_videoRoomTimelineView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_videoRoomTimelineView-iPhone-16-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_videoRoomTimelineView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_videoRoomTimelineView-iPhone-16-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user