refactor link preview view with SwiftUI

This commit is contained in:
Ryan Zhao 2023-09-11 16:48:38 +10:00
parent a576037cf5
commit a943df4f9e
2 changed files with 273 additions and 192 deletions

View File

@ -6,31 +6,110 @@ import SessionUIKit
import SessionMessagingKit
public struct LinkPreviewView_SwiftUI: View {
private var state: LinkPreviewState
private var isOutgoing: Bool
private let maxWidth: CGFloat
private var messageViewModel: MessageViewModel?
private var bodyLabelTextColor: ThemeValue?
private var lastSearchText: String?
private let onCancel: (() -> ())?
private static let loaderSize: CGFloat = 24
private static let cancelButtonSize: CGFloat = 45
private let maxWidth: CGFloat
private let onCancel: (() -> ())?
public init(maxWidth: CGFloat, onCancel: (() -> ())? = nil) {
init(
state: LinkPreviewState,
isOutgoing: Bool,
maxWidth: CGFloat = .infinity,
messageViewModel: MessageViewModel? = nil,
bodyLabelTextColor: ThemeValue? = nil,
lastSearchText: String? = nil,
onCancel: (() -> ())? = nil
) {
self.state = state
self.isOutgoing = isOutgoing
self.maxWidth = maxWidth
self.messageViewModel = messageViewModel
self.bodyLabelTextColor = bodyLabelTextColor
self.lastSearchText = lastSearchText
self.onCancel = onCancel
}
public var body: some View {
VStack(
alignment: .leading,
spacing: 0
spacing: Values.mediumSpacing
) {
HStack(
alignment: .center,
spacing: 0
spacing: Values.mediumSpacing
) {
// Link preview image
let imageSize: CGFloat = state is LinkPreview.SentState ? 100 : 80
if let linkPreviewImage: UIImage = state.image {
Image(uiImage: linkPreviewImage)
.resizable()
.scaledToFill()
.foregroundColor(
themeColor: isOutgoing ?
.messageBubble_outgoingText :
.messageBubble_incomingText
)
.frame(
width: imageSize,
height: imageSize
)
.cornerRadius(state is LinkPreview.SentState ? 0 : 8)
} else {
if
state is LinkPreview.DraftState || state is LinkPreview.SentState,
let defaultImage: UIImage = UIImage(named: "Link")?.withRenderingMode(.alwaysTemplate)
{
Image(uiImage: defaultImage)
.foregroundColor(
themeColor: isOutgoing ?
.messageBubble_outgoingText :
.messageBubble_incomingText
)
.frame(
width: imageSize,
height: imageSize
)
.cornerRadius(state is LinkPreview.SentState ? 0 : 8)
}
}
// Link preview title
if let title: String = state.title {
Text(title)
.bold()
.font(.system(size: Values.smallFontSize))
.multilineTextAlignment(.leading)
.foregroundColor(
themeColor: isOutgoing ?
.messageBubble_outgoingText :
.messageBubble_incomingText
)
}
// Cancel button
if state is LinkPreview.DraftState {
Spacer(minLength: 0)
Button(action: {
onCancel?()
}, label: {
if let image: UIImage = UIImage(named: "X")?.withRenderingMode(.alwaysTemplate) {
Image(uiImage: image)
.foregroundColor(themeColor: .textPrimary)
}
})
.frame(
width: Self.cancelButtonSize,
height: Self.cancelButtonSize
)
}
}
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
}
@ -38,8 +117,15 @@ public struct LinkPreviewView_SwiftUI: View {
struct LinkPreviewView_SwiftUI_Previews: PreviewProvider {
static var previews: some View {
LinkPreviewView_SwiftUI(
maxWidth: 200,
onCancel: nil
state: LinkPreview.DraftState(
linkPreviewDraft: .init(
urlString: "https://github.com/oxen-io",
title: "Github - oxen-io/session-ios: A private messenger for iOS.",
jpegImageData: UIImage(named: "AppIcon")?.jpegData(compressionQuality: 1)
)
),
isOutgoing: true
)
.padding(.horizontal, Values.mediumSpacing)
}
}

View File

@ -370,188 +370,183 @@ struct MessageBubble: View {
var body: some View {
ZStack {
switch messageViewModel.cellType {
case .typingIndicator, .dateHeader, .unreadMarker: break
case .textOnlyMessage:
let inset: CGFloat = 12
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: messageViewModel) - 2 * inset)
if let linkPreview: LinkPreview = messageViewModel.linkPreview {
switch linkPreview.variant {
case .standard:
let linkPreviewView: LinkPreviewView = LinkPreviewView(maxWidth: maxWidth)
linkPreviewView.update(
with: LinkPreview.SentState(
linkPreview: linkPreview,
imageAttachment: messageViewModel.linkPreviewAttachment
),
isOutgoing: (messageViewModel.variant == .standardOutgoing),
delegate: self,
cellViewModel: messageViewModel,
bodyLabelTextColor: bodyLabelTextColor,
lastSearchText: lastSearchText
)
bubbleView.addSubview(linkPreviewView)
linkPreviewView.pin(to: bubbleView, withInset: 0)
snContentView.addArrangedSubview(bubbleBackgroundView)
self.bodyTappableLabel = linkPreviewView.bodyTappableLabel
case .openGroupInvitation:
let openGroupInvitationView: OpenGroupInvitationView = OpenGroupInvitationView(
name: (linkPreview.title ?? ""),
url: linkPreview.url,
textColor: bodyLabelTextColor,
isOutgoing: (cellViewModel.variant == .standardOutgoing)
)
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,
currentUserPublicKey: cellViewModel.currentUserPublicKey,
currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey,
direction: (cellViewModel.variant == .standardOutgoing ?
.outgoing :
.incoming
),
attachment: cellViewModel.quoteAttachment,
hInset: hInset,
maxWidth: maxWidth
)
let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
stackView.addArrangedSubview(quoteViewContainer)
}
// Body text view
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
for: cellViewModel,
with: maxWidth,
textColor: bodyLabelTextColor,
searchText: lastSearchText,
delegate: self
)
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)
}
case .mediaMessage:
// Body text view
if let body: String = cellViewModel.body, !body.isEmpty {
let inset: CGFloat = 12
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
for: cellViewModel,
with: maxWidth,
textColor: bodyLabelTextColor,
searchText: lastSearchText,
delegate: self
)
self.bodyTappableLabel = bodyTappableLabel
bubbleView.addSubview(bodyTappableLabel)
bodyTappableLabel.pin(to: bubbleView, withInset: inset)
snContentView.addArrangedSubview(bubbleBackgroundView)
}
// Album view
let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel)
let albumView = MediaAlbumView(
mediaCache: mediaCache,
items: (cellViewModel.attachments?
.filter { $0.isVisualMedia })
.defaulting(to: []),
isOutgoing: (cellViewModel.variant == .standardOutgoing),
maxMessageWidth: maxMessageWidth
)
self.albumView = albumView
let size = getSize(for: cellViewModel)
albumView.set(.width, to: size.width)
albumView.set(.height, to: size.height)
albumView.loadMedia()
snContentView.addArrangedSubview(albumView)
unloadContent = { albumView.unloadMedia() }
case .audio:
guard let attachment: Attachment = cellViewModel.attachments?.first(where: { $0.isAudio }) else {
return
}
let voiceMessageView: VoiceMessageView = VoiceMessageView()
voiceMessageView.update(
with: attachment,
isPlaying: (playbackInfo?.state == .playing),
progress: (playbackInfo?.progress ?? 0),
playbackRate: (playbackInfo?.playbackRate ?? 1),
oldPlaybackRate: (playbackInfo?.oldPlaybackRate ?? 1)
)
bubbleView.addSubview(voiceMessageView)
voiceMessageView.pin(to: bubbleView)
snContentView.addArrangedSubview(bubbleBackgroundView)
self.voiceMessageView = voiceMessageView
case .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)
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
)
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)
}
// switch messageViewModel.cellType {
// case .typingIndicator, .dateHeader, .unreadMarker: break
//
// case .textOnlyMessage:
// let inset: CGFloat = 12
// let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: messageViewModel) - 2 * inset)
//
// if let linkPreview: LinkPreview = messageViewModel.linkPreview {
// switch linkPreview.variant {
// case .standard:
// LinkPreviewView_SwiftUI(
// state: LinkPreview.SentState(
// linkPreview: linkPreview,
// imageAttachment: messageViewModel.linkPreviewAttachment
// ),
// isOutgoing: (messageViewModel.variant == .standardOutgoing),
// maxWidth: maxWidth,
// messageViewModel: messageViewModel,
// bodyLabelTextColor: nil,
// lastSearchText: nil
// )
//
// case .openGroupInvitation:
// let openGroupInvitationView: OpenGroupInvitationView = OpenGroupInvitationView(
// name: (linkPreview.title ?? ""),
// url: linkPreview.url,
// textColor: bodyLabelTextColor,
// isOutgoing: (cellViewModel.variant == .standardOutgoing)
// )
// 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,
// currentUserPublicKey: cellViewModel.currentUserPublicKey,
// currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey,
// currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey,
// direction: (cellViewModel.variant == .standardOutgoing ?
// .outgoing :
// .incoming
// ),
// attachment: cellViewModel.quoteAttachment,
// hInset: hInset,
// maxWidth: maxWidth
// )
// let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
// stackView.addArrangedSubview(quoteViewContainer)
// }
//
// // Body text view
// let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
// for: cellViewModel,
// with: maxWidth,
// textColor: bodyLabelTextColor,
// searchText: lastSearchText,
// delegate: self
// )
// 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)
// }
//
// case .mediaMessage:
// // Body text view
// if let body: String = cellViewModel.body, !body.isEmpty {
// let inset: CGFloat = 12
// let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
// let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
// for: cellViewModel,
// with: maxWidth,
// textColor: bodyLabelTextColor,
// searchText: lastSearchText,
// delegate: self
// )
//
// self.bodyTappableLabel = bodyTappableLabel
// bubbleView.addSubview(bodyTappableLabel)
// bodyTappableLabel.pin(to: bubbleView, withInset: inset)
// snContentView.addArrangedSubview(bubbleBackgroundView)
// }
//
// // Album view
// let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel)
// let albumView = MediaAlbumView(
// mediaCache: mediaCache,
// items: (cellViewModel.attachments?
// .filter { $0.isVisualMedia })
// .defaulting(to: []),
// isOutgoing: (cellViewModel.variant == .standardOutgoing),
// maxMessageWidth: maxMessageWidth
// )
// self.albumView = albumView
// let size = getSize(for: cellViewModel)
// albumView.set(.width, to: size.width)
// albumView.set(.height, to: size.height)
// albumView.loadMedia()
// snContentView.addArrangedSubview(albumView)
//
// unloadContent = { albumView.unloadMedia() }
//
// case .audio:
// guard let attachment: Attachment = cellViewModel.attachments?.first(where: { $0.isAudio }) else {
// return
// }
//
// let voiceMessageView: VoiceMessageView = VoiceMessageView()
// voiceMessageView.update(
// with: attachment,
// isPlaying: (playbackInfo?.state == .playing),
// progress: (playbackInfo?.progress ?? 0),
// playbackRate: (playbackInfo?.playbackRate ?? 1),
// oldPlaybackRate: (playbackInfo?.oldPlaybackRate ?? 1)
// )
//
// bubbleView.addSubview(voiceMessageView)
// voiceMessageView.pin(to: bubbleView)
// snContentView.addArrangedSubview(bubbleBackgroundView)
// self.voiceMessageView = voiceMessageView
//
// case .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)
// 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
// )
//
// 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)
// }
}
.background(