Merge pull request #411 from mpretty-cyro/fix/quote-with-attachment

Added support for rendering quotes with attachments and bug fixes
pull/1061/head
Morgan Pretty 6 months ago committed by GitHub
commit 4dbe3b5c4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -934,25 +934,23 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold
// MARK: - Functions // MARK: - Functions
public func updateDraft(to draft: String) { public func updateDraft(to draft: String) {
let threadId: String = self.threadId /// Kick off an async process to save the `draft` message to the conversation (don't want to block the UI while doing this,
let currentDraft: String = dependencies[singleton: .storage] /// worst case the `draft` just won't be saved)
.read { db in dependencies[singleton: .storage]
.readPublisher { [threadId] db in
try SessionThread try SessionThread
.select(.messageDraft) .select(.messageDraft)
.filter(id: threadId) .filter(id: threadId)
.asRequest(of: String.self) .asRequest(of: String.self)
.fetchOne(db) .fetchOne(db)
} }
.defaulting(to: "") .filter { existingDraft -> Bool in draft != existingDraft }
.flatMapStorageWritePublisher(using: dependencies) { [threadId] db, _ in
// Only write the updated draft to the database if it's changed (avoid unnecessary writes) try SessionThread
guard draft != currentDraft else { return } .filter(id: threadId)
.updateAll(db, SessionThread.Columns.messageDraft.set(to: draft))
dependencies[singleton: .storage].writeAsync { db in }
try SessionThread .sinkUntilComplete()
.filter(id: threadId)
.updateAll(db, SessionThread.Columns.messageDraft.set(to: draft))
}
} }
/// This method indicates whether the client should try to mark the thread or it's messages as read (it's an optimisation for fully read /// This method indicates whether the client should try to mark the thread or it's messages as read (it's an optimisation for fully read

@ -470,7 +470,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
documentView = nil documentView = nil
bodyTappableLabel = nil bodyTappableLabel = nil
// Handle the deleted state first (it's much simpler than the others) /// These variants have no content so do nothing after cleaning up old state
guard
cellViewModel.cellType != .typingIndicator &&
cellViewModel.cellType != .dateHeader &&
cellViewModel.cellType != .unreadMarker
else { return }
/// Handle the deleted state first (it's much simpler than the others)
guard !cellViewModel.variant.isDeletedMessage else { guard !cellViewModel.variant.isDeletedMessage else {
let inset: CGFloat = 12 let inset: CGFloat = 12
let deletedMessageView: DeletedMessageView = DeletedMessageView( let deletedMessageView: DeletedMessageView = DeletedMessageView(
@ -484,125 +491,223 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
return return
} }
// If it's an incoming media message and the thread isn't trusted then show the placeholder view /// The `textOnlyMessage` variant has a slightly different behaviour (as it's the only variant which supports link previews)
if cellViewModel.cellType != .textOnlyMessage && cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted { /// so we handle that case first
let mediaPlaceholderView = MediaPlaceholderView(cellViewModel: cellViewModel, textColor: bodyLabelTextColor) // FIXME: We should support rendering link previews alongside the other variants (bigger refactor)
bubbleView.addSubview(mediaPlaceholderView) guard cellViewModel.cellType != .textOnlyMessage else {
mediaPlaceholderView.pin(to: bubbleView) let inset: CGFloat = 12
snContentView.addArrangedSubview(bubbleBackgroundView) let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
return
}
switch cellViewModel.cellType {
case .typingIndicator, .dateHeader, .unreadMarker: break
case .textOnlyMessage: if let linkPreview: LinkPreview = cellViewModel.linkPreview {
let inset: CGFloat = 12 switch linkPreview.variant {
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) case .standard:
let linkPreviewView: LinkPreviewView = LinkPreviewView(maxWidth: maxWidth)
if let linkPreview: LinkPreview = cellViewModel.linkPreview { linkPreviewView.update(
switch linkPreview.variant { with: LinkPreview.SentState(
case .standard: linkPreview: linkPreview,
let linkPreviewView: LinkPreviewView = LinkPreviewView(maxWidth: maxWidth) imageAttachment: cellViewModel.linkPreviewAttachment,
linkPreviewView.update(
with: LinkPreview.SentState(
linkPreview: linkPreview,
imageAttachment: cellViewModel.linkPreviewAttachment,
using: dependencies
),
isOutgoing: cellViewModel.variant.isOutgoing,
delegate: self,
cellViewModel: cellViewModel,
bodyLabelTextColor: bodyLabelTextColor,
lastSearchText: lastSearchText,
using: dependencies using: dependencies
) ),
self.linkPreviewView = linkPreviewView isOutgoing: cellViewModel.variant.isOutgoing,
bubbleView.addSubview(linkPreviewView) delegate: self,
linkPreviewView.pin(to: bubbleView, withInset: 0) cellViewModel: cellViewModel,
snContentView.addArrangedSubview(bubbleBackgroundView) bodyLabelTextColor: bodyLabelTextColor,
self.bodyTappableLabel = linkPreviewView.bodyTappableLabel lastSearchText: lastSearchText,
case .openGroupInvitation:
let openGroupInvitationView: OpenGroupInvitationView = OpenGroupInvitationView(
name: (linkPreview.title ?? ""),
url: linkPreview.url,
textColor: bodyLabelTextColor,
isOutgoing: cellViewModel.variant.isOutgoing
)
openGroupInvitationView.isAccessibilityElement = true
openGroupInvitationView.accessibilityIdentifier = "Community invitation"
openGroupInvitationView.accessibilityLabel = cellViewModel.linkPreview?.title
bubbleView.addSubview(openGroupInvitationView)
bubbleView.pin(to: openGroupInvitationView)
snContentView.addArrangedSubview(bubbleBackgroundView)
}
}
else {
// Stack view
let stackView = UIStackView(arrangedSubviews: [])
stackView.axis = .vertical
stackView.spacing = 2
// Quote view
if let quote: Quote = cellViewModel.quote {
let hInset: CGFloat = 2
let quoteView: QuoteView = QuoteView(
for: .regular,
authorId: quote.authorId,
quotedText: quote.body,
threadVariant: cellViewModel.threadVariant,
currentUserSessionId: cellViewModel.currentUserSessionId,
currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId,
currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId,
direction: (cellViewModel.variant.isOutgoing ? .outgoing : .incoming),
attachment: cellViewModel.quoteAttachment,
using: dependencies using: dependencies
) )
self.quoteView = quoteView self.linkPreviewView = linkPreviewView
let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset)) bubbleView.addSubview(linkPreviewView)
stackView.addArrangedSubview(quoteViewContainer) linkPreviewView.pin(to: bubbleView, withInset: 0)
} snContentView.addArrangedSubview(bubbleBackgroundView)
self.bodyTappableLabel = linkPreviewView.bodyTappableLabel
// Body text view
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel( case .openGroupInvitation:
for: cellViewModel, let openGroupInvitationView: OpenGroupInvitationView = OpenGroupInvitationView(
with: maxWidth, name: (linkPreview.title ?? ""),
textColor: bodyLabelTextColor, url: linkPreview.url,
searchText: lastSearchText, textColor: bodyLabelTextColor,
delegate: self, isOutgoing: cellViewModel.variant.isOutgoing
)
openGroupInvitationView.isAccessibilityElement = true
openGroupInvitationView.accessibilityIdentifier = "Community invitation"
openGroupInvitationView.accessibilityLabel = cellViewModel.linkPreview?.title
bubbleView.addSubview(openGroupInvitationView)
bubbleView.pin(to: openGroupInvitationView)
snContentView.addArrangedSubview(bubbleBackgroundView)
}
}
else {
// Stack view
let stackView = UIStackView(arrangedSubviews: [])
stackView.axis = .vertical
stackView.spacing = 2
// Quote view
if let quote: Quote = cellViewModel.quote {
let hInset: CGFloat = 2
let quoteView: QuoteView = QuoteView(
for: .regular,
authorId: quote.authorId,
quotedText: quote.body,
threadVariant: cellViewModel.threadVariant,
currentUserSessionId: cellViewModel.currentUserSessionId,
currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId,
currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId,
direction: (cellViewModel.variant.isOutgoing ? .outgoing : .incoming),
attachment: cellViewModel.quoteAttachment,
using: dependencies using: dependencies
) )
self.bodyTappableLabel = bodyTappableLabel self.quoteView = quoteView
stackView.addArrangedSubview(bodyTappableLabel) let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
stackView.addArrangedSubview(quoteViewContainer)
// Constraints
bubbleView.addSubview(stackView)
stackView.pin(to: bubbleView, withInset: inset)
stackView.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
snContentView.addArrangedSubview(bubbleBackgroundView)
} }
case .mediaMessage:
// Body text view // Body text view
if let body: String = cellViewModel.body, !body.isEmpty { let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
let inset: CGFloat = 12 for: cellViewModel,
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) with: maxWidth,
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel( textColor: bodyLabelTextColor,
for: cellViewModel, searchText: lastSearchText,
with: maxWidth, delegate: self,
textColor: bodyLabelTextColor, using: dependencies
searchText: lastSearchText, )
delegate: self, self.bodyTappableLabel = bodyTappableLabel
using: dependencies stackView.addArrangedSubview(bodyTappableLabel)
)
// Constraints
self.bodyTappableLabel = bodyTappableLabel bubbleView.addSubview(stackView)
bubbleView.addSubview(bodyTappableLabel) stackView.pin(to: bubbleView, withInset: inset)
bodyTappableLabel.pin(to: bubbleView, withInset: inset) stackView.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
snContentView.addArrangedSubview(bubbleBackgroundView)
}
return
}
func addViewWrappingInBubbleIfNeeded(_ targetView: UIView) {
switch snContentView.arrangedSubviews.count {
case 0:
bubbleView.addSubview(targetView)
targetView.pin(to: bubbleView)
snContentView.addArrangedSubview(bubbleBackgroundView) snContentView.addArrangedSubview(bubbleBackgroundView)
}
default:
/// Since we already have content we need to wrap the `targetView` in it's own
/// `bubbleView` (as it's likely the existing content is quote content)
let extraBubbleView: UIView = UIView()
extraBubbleView.clipsToBounds = true
extraBubbleView.themeBackgroundColor = (cellViewModel.variant.isIncoming ?
.messageBubble_incomingBackground :
.messageBubble_outgoingBackground
)
extraBubbleView.layer.cornerRadius = VisibleMessageCell.largeCornerRadius
extraBubbleView.layer.maskedCorners = getCornerMask(from: .allCorners)
extraBubbleView.set(.width, greaterThanOrEqualTo: VisibleMessageCell.largeCornerRadius * 2)
extraBubbleView.addSubview(targetView)
targetView.pin(to: extraBubbleView)
snContentView.addArrangedSubview(extraBubbleView)
}
}
/// Add any quote & body if present
let inset: CGFloat = 12
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
switch (cellViewModel.quote, cellViewModel.body) {
/// Both quote and body
case (.some(let quote), .some(let body)) where !body.isEmpty:
// Stack view
let stackView = UIStackView(arrangedSubviews: [])
stackView.axis = .vertical
stackView.spacing = 2
// Quote view
let hInset: CGFloat = 2
let quoteView: QuoteView = QuoteView(
for: .regular,
authorId: quote.authorId,
quotedText: quote.body,
threadVariant: cellViewModel.threadVariant,
currentUserSessionId: cellViewModel.currentUserSessionId,
currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId,
currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId,
direction: (cellViewModel.variant.isOutgoing ? .outgoing : .incoming),
attachment: cellViewModel.quoteAttachment,
using: dependencies
)
self.quoteView = quoteView
let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
stackView.addArrangedSubview(quoteViewContainer)
// Body
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
for: cellViewModel,
with: maxWidth,
textColor: bodyLabelTextColor,
searchText: lastSearchText,
delegate: self,
using: dependencies
)
self.bodyTappableLabel = bodyTappableLabel
stackView.addArrangedSubview(bodyTappableLabel)
// Constraints
bubbleView.addSubview(stackView)
stackView.pin(to: bubbleView, withInset: inset)
stackView.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
snContentView.addArrangedSubview(bubbleBackgroundView)
/// Just body
case (_, .some(let body)) where !body.isEmpty:
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
for: cellViewModel,
with: maxWidth,
textColor: bodyLabelTextColor,
searchText: lastSearchText,
delegate: self,
using: dependencies
)
self.bodyTappableLabel = bodyTappableLabel
bubbleView.addSubview(bodyTappableLabel)
bodyTappableLabel.pin(to: bubbleView, withInset: inset)
snContentView.addArrangedSubview(bubbleBackgroundView)
/// Just quote
case (.some(let quote), _):
let quoteView: QuoteView = QuoteView(
for: .regular,
authorId: quote.authorId,
quotedText: quote.body,
threadVariant: cellViewModel.threadVariant,
currentUserSessionId: cellViewModel.currentUserSessionId,
currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId,
currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId,
direction: (cellViewModel.variant.isOutgoing ? .outgoing : .incoming),
attachment: cellViewModel.quoteAttachment,
using: dependencies
)
self.quoteView = quoteView
bubbleView.addSubview(quoteView)
quoteView.pin(to: bubbleView, withInset: inset)
snContentView.addArrangedSubview(bubbleBackgroundView)
/// Neither quote or body
default: break
}
/// If it's an incoming media message and the thread isn't trusted then show the placeholder view
if cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted {
let mediaPlaceholderView = MediaPlaceholderView(cellViewModel: cellViewModel, textColor: bodyLabelTextColor)
addViewWrappingInBubbleIfNeeded(mediaPlaceholderView)
return
}
switch cellViewModel.cellType {
case .typingIndicator, .dateHeader, .unreadMarker, .textOnlyMessage: break
case .mediaMessage:
// Album view // Album view
let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel) let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel)
let albumView = MediaAlbumView( let albumView = MediaAlbumView(
@ -637,52 +742,16 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
playbackRate: (playbackInfo?.playbackRate ?? 1), playbackRate: (playbackInfo?.playbackRate ?? 1),
oldPlaybackRate: (playbackInfo?.oldPlaybackRate ?? 1) oldPlaybackRate: (playbackInfo?.oldPlaybackRate ?? 1)
) )
bubbleView.addSubview(voiceMessageView)
voiceMessageView.pin(to: bubbleView)
snContentView.addArrangedSubview(bubbleBackgroundView)
self.voiceMessageView = voiceMessageView self.voiceMessageView = voiceMessageView
addViewWrappingInBubbleIfNeeded(voiceMessageView)
case .audio, .genericAttachment: case .audio, .genericAttachment:
guard let attachment: Attachment = cellViewModel.attachments?.first else { preconditionFailure() } guard let attachment: Attachment = cellViewModel.attachments?.first else { preconditionFailure() }
let inset: CGFloat = 12
let maxWidth = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
// Stack view
let stackView = UIStackView(arrangedSubviews: [])
stackView.axis = .vertical
stackView.spacing = Values.smallSpacing
// Document view // Document view
let documentView = DocumentView(attachment: attachment, textColor: bodyLabelTextColor) let documentView = DocumentView(attachment: attachment, textColor: bodyLabelTextColor)
self.documentView = documentView self.documentView = documentView
stackView.addArrangedSubview(documentView) addViewWrappingInBubbleIfNeeded(documentView)
// Body text view
if let body: String = cellViewModel.body, !body.isEmpty { // delegate should always be set at this point
let bodyContainerView: UIView = UIView()
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
for: cellViewModel,
with: maxWidth,
textColor: bodyLabelTextColor,
searchText: lastSearchText,
delegate: self,
using: dependencies
)
self.bodyTappableLabel = bodyTappableLabel
bodyContainerView.addSubview(bodyTappableLabel)
bodyTappableLabel.pin(.top, to: .top, of: bodyContainerView)
bodyTappableLabel.pin(.leading, to: .leading, of: bodyContainerView, withInset: 12)
bodyTappableLabel.pin(.trailing, to: .trailing, of: bodyContainerView, withInset: -12)
bodyTappableLabel.pin(.bottom, to: .bottom, of: bodyContainerView, withInset: -12)
stackView.addArrangedSubview(bodyContainerView)
}
bubbleView.addSubview(stackView)
stackView.pin(to: bubbleView)
snContentView.addArrangedSubview(bubbleBackgroundView)
} }
} }

@ -28,116 +28,154 @@ struct MessageInfoScreen: View {
alignment: .leading, alignment: .leading,
spacing: 10 spacing: 10
) { ) {
// Message bubble snapshot VStack(
MessageBubble( alignment: .leading,
messageViewModel: messageViewModel, spacing: 0
dependencies: dependencies ) {
) // Message bubble snapshot
.clipShape( MessageBubble(
RoundedRectangle(cornerRadius: Self.cornerRadius) messageViewModel: messageViewModel,
) attachmentOnly: false,
.background( dependencies: dependencies
RoundedRectangle(cornerRadius: Self.cornerRadius) )
.fill( .clipShape(
themeColor: (messageViewModel.variant == .standardIncoming || messageViewModel.variant == .standardIncomingDeleted || messageViewModel.variant == .standardIncomingDeletedLocally ? RoundedRectangle(cornerRadius: Self.cornerRadius)
.messageBubble_incomingBackground : )
.messageBubble_outgoingBackground) .background(
) RoundedRectangle(cornerRadius: Self.cornerRadius)
) .fill(
.frame( themeColor: (messageViewModel.variant == .standardIncoming || messageViewModel.variant == .standardIncomingDeleted || messageViewModel.variant == .standardIncomingDeletedLocally ?
maxWidth: .infinity, .messageBubble_incomingBackground :
maxHeight: .infinity, .messageBubble_outgoingBackground)
alignment: .topLeading )
)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, Values.smallSpacing)
.padding(.bottom, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
if isMessageFailed {
let (image, statusText, tintColor) = messageViewModel.state.statusIconInfo(
variant: messageViewModel.variant,
hasBeenReadByRecipient: messageViewModel.hasBeenReadByRecipient,
hasAttachments: (messageViewModel.attachments?.isEmpty == false)
) )
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, Values.smallSpacing)
.padding(.bottom, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
HStack(spacing: 6) {
if let image: UIImage = image?.withRenderingMode(.alwaysTemplate) { if isMessageFailed {
Image(uiImage: image) let (image, statusText, tintColor) = messageViewModel.state.statusIconInfo(
.resizable() variant: messageViewModel.variant,
.scaledToFit() hasBeenReadByRecipient: messageViewModel.hasBeenReadByRecipient,
.foregroundColor(themeColor: tintColor) hasAttachments: (messageViewModel.attachments?.isEmpty == false)
.frame(width: 13, height: 12) )
}
if let statusText: String = statusText { HStack(spacing: 6) {
Text(statusText) if let image: UIImage = image?.withRenderingMode(.alwaysTemplate) {
.font(.system(size: Values.verySmallFontSize)) Image(uiImage: image)
.foregroundColor(themeColor: tintColor) .resizable()
.scaledToFit()
.foregroundColor(themeColor: tintColor)
.frame(width: 13, height: 12)
}
if let statusText: String = statusText {
Text(statusText)
.font(.system(size: Values.verySmallFontSize))
.foregroundColor(themeColor: tintColor)
}
} }
.padding(.top, -Values.smallSpacing)
.padding(.bottom, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
} }
.padding(.top, -Values.smallSpacing)
.padding(.bottom, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
}
if let attachments = messageViewModel.attachments,
messageViewModel.cellType == .mediaMessage
{
let attachment: Attachment = attachments[(index - 1 + attachments.count) % attachments.count]
ZStack(alignment: .bottomTrailing) { if let attachments = messageViewModel.attachments {
if attachments.count > 1 { switch messageViewModel.cellType {
// Attachment carousel view case .mediaMessage:
SessionCarouselView_SwiftUI( let attachment: Attachment = attachments[(index - 1 + attachments.count) % attachments.count]
index: $index,
isOutgoing: (messageViewModel.variant == .standardOutgoing), ZStack(alignment: .bottomTrailing) {
contentInfos: attachments, if attachments.count > 1 {
using: dependencies // Attachment carousel view
) SessionCarouselView_SwiftUI(
.frame( index: $index,
maxWidth: .infinity, isOutgoing: (messageViewModel.variant == .standardOutgoing),
maxHeight: .infinity, contentInfos: attachments,
alignment: .topLeading using: dependencies
) )
} else { .frame(
MediaView_SwiftUI( maxWidth: .infinity,
attachment: attachments[0], maxHeight: .infinity,
isOutgoing: (messageViewModel.variant == .standardOutgoing), alignment: .topLeading
shouldSupressControls: true, )
cornerRadius: 0, } else {
using: dependencies MediaView_SwiftUI(
) attachment: attachments[0],
.frame( isOutgoing: (messageViewModel.variant == .standardOutgoing),
maxWidth: .infinity, shouldSupressControls: true,
maxHeight: .infinity, cornerRadius: 0,
alignment: .topLeading using: dependencies
) )
.aspectRatio(1, contentMode: .fit) .frame(
.clipShape(RoundedRectangle(cornerRadius: 15)) maxWidth: .infinity,
.padding(.horizontal, Values.largeSpacing) maxHeight: .infinity,
} alignment: .topLeading
)
if [ .downloaded, .uploaded ].contains(attachment.state) { .aspectRatio(1, contentMode: .fit)
Button { .clipShape(RoundedRectangle(cornerRadius: 15))
self.showMediaFullScreen(attachment: attachment) .padding(.horizontal, Values.largeSpacing)
} label: { }
ZStack {
Circle() if [ .downloaded, .uploaded ].contains(attachment.state) {
.foregroundColor(.init(white: 0, opacity: 0.4)) Button {
Image(systemName: "arrow.up.left.and.arrow.down.right") self.showMediaFullScreen(attachment: attachment)
.font(.system(size: 13)) } label: {
.foregroundColor(.white) ZStack {
Circle()
.foregroundColor(.init(white: 0, opacity: 0.4))
Image(systemName: "arrow.up.left.and.arrow.down.right")
.font(.system(size: 13))
.foregroundColor(.white)
}
.frame(width: 26, height: 26)
}
.padding(.bottom, Values.smallSpacing)
.padding(.trailing, 38)
}
} }
.frame(width: 26, height: 26) .padding(.vertical, Values.verySmallSpacing)
}
.padding(.bottom, Values.smallSpacing) default:
.padding(.trailing, 38) MessageBubble(
messageViewModel: messageViewModel,
attachmentOnly: true,
dependencies: dependencies
)
.clipShape(
RoundedRectangle(cornerRadius: Self.cornerRadius)
)
.background(
RoundedRectangle(cornerRadius: Self.cornerRadius)
.fill(
themeColor: (messageViewModel.variant == .standardIncoming || messageViewModel.variant == .standardIncomingDeleted || messageViewModel.variant == .standardIncomingDeletedLocally ?
.messageBubble_incomingBackground :
.messageBubble_outgoingBackground)
)
)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.fixedSize(horizontal: false, vertical: true)
.padding(.bottom, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
} }
} }
.padding(.vertical, Values.verySmallSpacing) }
// Attachment Info
if let attachments = messageViewModel.attachments {
let attachment: Attachment = attachments[(index - 1 + attachments.count) % attachments.count]
// Attachment Info
ZStack { ZStack {
VStack( VStack(
alignment: .leading, alignment: .leading,
@ -394,6 +432,7 @@ struct MessageBubble: View {
static private let inset: CGFloat = 12 static private let inset: CGFloat = 12
let messageViewModel: MessageViewModel let messageViewModel: MessageViewModel
let attachmentOnly: Bool
let dependencies: Dependencies let dependencies: Dependencies
var bodyLabelTextColor: ThemeValue { var bodyLabelTextColor: ThemeValue {
@ -404,16 +443,16 @@ struct MessageBubble: View {
var body: some View { var body: some View {
ZStack { ZStack {
switch messageViewModel.cellType { let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: messageViewModel) - 2 * Self.inset)
case .textOnlyMessage:
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: messageViewModel) - 2 * Self.inset) VStack(
alignment: .leading,
VStack( spacing: 0
alignment: .leading, ) {
spacing: 0 if !attachmentOnly {
) { // FIXME: We should support rendering link previews alongside quotes (bigger refactor)
if let linkPreview: LinkPreview = messageViewModel.linkPreview { if let linkPreview: LinkPreview = messageViewModel.linkPreview {
switch linkPreview.variant { switch linkPreview.variant {
case .standard: case .standard:
LinkPreviewView_SwiftUI( LinkPreviewView_SwiftUI(
state: LinkPreview.SentState( state: LinkPreview.SentState(
@ -434,44 +473,34 @@ struct MessageBubble: View {
url: linkPreview.url, url: linkPreview.url,
textColor: bodyLabelTextColor, textColor: bodyLabelTextColor,
isOutgoing: (messageViewModel.variant == .standardOutgoing)) isOutgoing: (messageViewModel.variant == .standardOutgoing))
}
}
else {
if let quote = messageViewModel.quote {
QuoteView_SwiftUI(
info: .init(
mode: .regular,
authorId: quote.authorId,
quotedText: quote.body,
threadVariant: messageViewModel.threadVariant,
currentUserSessionId: messageViewModel.currentUserSessionId,
currentUserBlinded15SessionId: messageViewModel.currentUserBlinded15SessionId,
currentUserBlinded25SessionId: messageViewModel.currentUserBlinded25SessionId,
direction: (messageViewModel.variant == .standardOutgoing ? .outgoing : .incoming),
attachment: messageViewModel.quoteAttachment
),
using: dependencies
)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, Self.inset)
.padding(.horizontal, Self.inset)
.padding(.bottom, -Values.smallSpacing)
}
} }
}
if let bodyText: NSAttributedString = VisibleMessageCell.getBodyAttributedText( else {
for: messageViewModel, if let quote = messageViewModel.quote {
theme: ThemeManager.currentTheme, QuoteView_SwiftUI(
primaryColor: ThemeManager.primaryColor, info: .init(
textColor: bodyLabelTextColor, mode: .regular,
searchText: nil, authorId: quote.authorId,
using: dependencies quotedText: quote.body,
) { threadVariant: messageViewModel.threadVariant,
AttributedText(bodyText) currentUserSessionId: messageViewModel.currentUserSessionId,
.padding(.all, Self.inset) currentUserBlinded15SessionId: messageViewModel.currentUserBlinded15SessionId,
currentUserBlinded25SessionId: messageViewModel.currentUserBlinded25SessionId,
direction: (messageViewModel.variant == .standardOutgoing ? .outgoing : .incoming),
attachment: messageViewModel.quoteAttachment
),
using: dependencies
)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, Self.inset)
.padding(.horizontal, Self.inset)
.padding(.bottom, (messageViewModel.body?.isEmpty == false ?
-Values.smallSpacing :
Self.inset
))
} }
} }
case .mediaMessage:
if let bodyText: NSAttributedString = VisibleMessageCell.getBodyAttributedText( if let bodyText: NSAttributedString = VisibleMessageCell.getBodyAttributedText(
for: messageViewModel, for: messageViewModel,
theme: ThemeManager.currentTheme, theme: ThemeManager.currentTheme,
@ -483,51 +512,30 @@ struct MessageBubble: View {
AttributedText(bodyText) AttributedText(bodyText)
.padding(.all, Self.inset) .padding(.all, Self.inset)
} }
case .voiceMessage: }
if let attachment: Attachment = messageViewModel.attachments?.first(where: { $0.isAudio }){ else {
// TODO: Playback Info and check if playing function is needed switch messageViewModel.cellType {
VoiceMessageView_SwiftUI(attachment: attachment) case .voiceMessage:
} if let attachment: Attachment = messageViewModel.attachments?.first(where: { $0.isAudio }){
case .audio, .genericAttachment: // TODO: Playback Info and check if playing function is needed
if let attachment: Attachment = messageViewModel.attachments?.first { VoiceMessageView_SwiftUI(attachment: attachment)
VStack( }
alignment: .leading, case .audio, .genericAttachment:
spacing: Values.smallSpacing if let attachment: Attachment = messageViewModel.attachments?.first {
) { DocumentView_SwiftUI(
DocumentView_SwiftUI( maxWidth: $maxWidth,
maxWidth: $maxWidth, attachment: attachment,
attachment: attachment, textColor: bodyLabelTextColor
textColor: bodyLabelTextColor )
)
.modifier(MaxWidthEqualizer.notify)
.frame(
width: maxWidth,
alignment: .leading
)
if let bodyText: NSAttributedString = VisibleMessageCell.getBodyAttributedText(
for: messageViewModel,
theme: ThemeManager.currentTheme,
primaryColor: ThemeManager.primaryColor,
textColor: bodyLabelTextColor,
searchText: nil,
using: dependencies
) {
ZStack{
AttributedText(bodyText)
.padding(.horizontal, Self.inset)
.padding(.bottom, Self.inset)
}
.modifier(MaxWidthEqualizer.notify) .modifier(MaxWidthEqualizer.notify)
.frame( .frame(
width: maxWidth, width: maxWidth,
alignment: .leading alignment: .leading
) )
} }
} default: EmptyView()
.modifier(MaxWidthEqualizer(width: $maxWidth))
} }
default: EmptyView() }
} }
} }
} }

@ -349,11 +349,6 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
// (the album view) // (the album view)
guard self.attachments?.count == 1 else { return .mediaMessage } guard self.attachments?.count == 1 else { return .mediaMessage }
// Quote and LinkPreview overload the 'attachments' array and use it for their
// own purposes, otherwise check if the attachment is visual media
guard self.quote == nil else { return .textOnlyMessage }
guard self.linkPreview == nil else { return .textOnlyMessage }
// Pending audio attachments won't have a duration // Pending audio attachments won't have a duration
if if
attachment.isAudio && ( attachment.isAudio && (

Loading…
Cancel
Save