From a943df4f9e87bff7750e2b33ff56fc003b38ba55 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 11 Sep 2023 16:48:38 +1000 Subject: [PATCH] refactor link preview view with SwiftUI --- .../LinkPreviewView_SwiftUI.swift | 106 +++++- .../MessageInfoView.swift | 359 +++++++++--------- 2 files changed, 273 insertions(+), 192 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/LinkPreviewView_SwiftUI.swift b/Session/Conversations/Message Cells/Content Views/LinkPreviewView_SwiftUI.swift index e7a769da4..7b33df46e 100644 --- a/Session/Conversations/Message Cells/Content Views/LinkPreviewView_SwiftUI.swift +++ b/Session/Conversations/Message Cells/Content Views/LinkPreviewView_SwiftUI.swift @@ -6,31 +6,110 @@ import SessionUIKit import SessionMessagingKit public struct LinkPreviewView_SwiftUI: View { - private static let loaderSize: CGFloat = 24 - private static let cancelButtonSize: CGFloat = 45 - + 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: (() -> ())? - public init(maxWidth: CGFloat, onCancel: (() -> ())? = nil) { + private static let loaderSize: CGFloat = 24 + private static let cancelButtonSize: CGFloat = 45 + + 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) } } diff --git a/Session/Media Viewing & Editing/MessageInfoView.swift b/Session/Media Viewing & Editing/MessageInfoView.swift index 283b9e777..4a06a888b 100644 --- a/Session/Media Viewing & Editing/MessageInfoView.swift +++ b/Session/Media Viewing & Editing/MessageInfoView.swift @@ -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(