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
public func updateDraft(to draft: String) {
let threadId: String = self.threadId
let currentDraft: String = dependencies[singleton: .storage]
.read { db in
/// Kick off an async process to save the `draft` message to the conversation (don't want to block the UI while doing this,
/// worst case the `draft` just won't be saved)
dependencies[singleton: .storage]
.readPublisher { [threadId] db in
try SessionThread
.select(.messageDraft)
.filter(id: threadId)
.asRequest(of: String.self)
.fetchOne(db)
}
.defaulting(to: "")
// Only write the updated draft to the database if it's changed (avoid unnecessary writes)
guard draft != currentDraft else { return }
dependencies[singleton: .storage].writeAsync { db in
.filter { existingDraft -> Bool in draft != existingDraft }
.flatMapStorageWritePublisher(using: dependencies) { [threadId] db, _ in
try SessionThread
.filter(id: threadId)
.updateAll(db, SessionThread.Columns.messageDraft.set(to: draft))
}
.sinkUntilComplete()
}
/// 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
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 {
let inset: CGFloat = 12
let deletedMessageView: DeletedMessageView = DeletedMessageView(
@ -484,19 +491,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
return
}
// If it's an incoming media message and the thread isn't trusted then show the placeholder view
if cellViewModel.cellType != .textOnlyMessage && cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted {
let mediaPlaceholderView = MediaPlaceholderView(cellViewModel: cellViewModel, textColor: bodyLabelTextColor)
bubbleView.addSubview(mediaPlaceholderView)
mediaPlaceholderView.pin(to: bubbleView)
snContentView.addArrangedSubview(bubbleBackgroundView)
return
}
switch cellViewModel.cellType {
case .typingIndicator, .dateHeader, .unreadMarker: break
case .textOnlyMessage:
/// The `textOnlyMessage` variant has a slightly different behaviour (as it's the only variant which supports link previews)
/// so we handle that case first
// FIXME: We should support rendering link previews alongside the other variants (bigger refactor)
guard cellViewModel.cellType != .textOnlyMessage else {
let inset: CGFloat = 12
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
@ -582,12 +580,85 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
stackView.widthAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
snContentView.addArrangedSubview(bubbleBackgroundView)
}
return
}
case .mediaMessage:
// Body text view
if let body: String = cellViewModel.body, !body.isEmpty {
func addViewWrappingInBubbleIfNeeded(_ targetView: UIView) {
switch snContentView.arrangedSubviews.count {
case 0:
bubbleView.addSubview(targetView)
targetView.pin(to: bubbleView)
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,
@ -601,8 +672,42 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
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
let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel)
let albumView = MediaAlbumView(
@ -637,52 +742,16 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
playbackRate: (playbackInfo?.playbackRate ?? 1),
oldPlaybackRate: (playbackInfo?.oldPlaybackRate ?? 1)
)
bubbleView.addSubview(voiceMessageView)
voiceMessageView.pin(to: bubbleView)
snContentView.addArrangedSubview(bubbleBackgroundView)
self.voiceMessageView = voiceMessageView
addViewWrappingInBubbleIfNeeded(voiceMessageView)
case .audio, .genericAttachment:
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
let documentView = DocumentView(attachment: attachment, textColor: bodyLabelTextColor)
self.documentView = documentView
stackView.addArrangedSubview(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)
addViewWrappingInBubbleIfNeeded(documentView)
}
}

@ -27,10 +27,15 @@ struct MessageInfoScreen: View {
VStack(
alignment: .leading,
spacing: 10
) {
VStack(
alignment: .leading,
spacing: 0
) {
// Message bubble snapshot
MessageBubble(
messageViewModel: messageViewModel,
attachmentOnly: false,
dependencies: dependencies
)
.clipShape(
@ -54,6 +59,7 @@ struct MessageInfoScreen: View {
.padding(.bottom, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
if isMessageFailed {
let (image, statusText, tintColor) = messageViewModel.state.statusIconInfo(
variant: messageViewModel.variant,
@ -81,9 +87,9 @@ struct MessageInfoScreen: View {
.padding(.horizontal, Values.largeSpacing)
}
if let attachments = messageViewModel.attachments,
messageViewModel.cellType == .mediaMessage
{
if let attachments = messageViewModel.attachments {
switch messageViewModel.cellType {
case .mediaMessage:
let attachment: Attachment = attachments[(index - 1 + attachments.count) % attachments.count]
ZStack(alignment: .bottomTrailing) {
@ -137,7 +143,39 @@ struct MessageInfoScreen: View {
}
.padding(.vertical, Values.verySmallSpacing)
default:
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)
}
}
}
// Attachment Info
if let attachments = messageViewModel.attachments {
let attachment: Attachment = attachments[(index - 1 + attachments.count) % attachments.count]
ZStack {
VStack(
alignment: .leading,
@ -394,6 +432,7 @@ struct MessageBubble: View {
static private let inset: CGFloat = 12
let messageViewModel: MessageViewModel
let attachmentOnly: Bool
let dependencies: Dependencies
var bodyLabelTextColor: ThemeValue {
@ -404,14 +443,14 @@ struct MessageBubble: View {
var body: some View {
ZStack {
switch messageViewModel.cellType {
case .textOnlyMessage:
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: messageViewModel) - 2 * Self.inset)
VStack(
alignment: .leading,
spacing: 0
) {
if !attachmentOnly {
// FIXME: We should support rendering link previews alongside quotes (bigger refactor)
if let linkPreview: LinkPreview = messageViewModel.linkPreview {
switch linkPreview.variant {
case .standard:
@ -455,7 +494,10 @@ struct MessageBubble: View {
.fixedSize(horizontal: false, vertical: true)
.padding(.top, Self.inset)
.padding(.horizontal, Self.inset)
.padding(.bottom, -Values.smallSpacing)
.padding(.bottom, (messageViewModel.body?.isEmpty == false ?
-Values.smallSpacing :
Self.inset
))
}
}
@ -471,18 +513,8 @@ struct MessageBubble: View {
.padding(.all, Self.inset)
}
}
case .mediaMessage:
if let bodyText: NSAttributedString = VisibleMessageCell.getBodyAttributedText(
for: messageViewModel,
theme: ThemeManager.currentTheme,
primaryColor: ThemeManager.primaryColor,
textColor: bodyLabelTextColor,
searchText: nil,
using: dependencies
) {
AttributedText(bodyText)
.padding(.all, Self.inset)
}
else {
switch messageViewModel.cellType {
case .voiceMessage:
if let attachment: Attachment = messageViewModel.attachments?.first(where: { $0.isAudio }){
// TODO: Playback Info and check if playing function is needed
@ -490,10 +522,6 @@ struct MessageBubble: View {
}
case .audio, .genericAttachment:
if let attachment: Attachment = messageViewModel.attachments?.first {
VStack(
alignment: .leading,
spacing: Values.smallSpacing
) {
DocumentView_SwiftUI(
maxWidth: $maxWidth,
attachment: attachment,
@ -504,30 +532,10 @@ struct MessageBubble: View {
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)
.frame(
width: maxWidth,
alignment: .leading
)
}
default: EmptyView()
}
.modifier(MaxWidthEqualizer(width: $maxWidth))
}
default: EmptyView()
}
}
}

@ -349,11 +349,6 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
// (the album view)
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
if
attachment.isAudio && (

Loading…
Cancel
Save