|
|
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
|
|
import SwiftUI
|
|
|
import SessionUIKit
|
|
|
import SessionSnodeKit
|
|
|
import SessionUtilitiesKit
|
|
|
import SessionMessagingKit
|
|
|
|
|
|
struct MessageInfoView: View {
|
|
|
@Environment(\.viewController) private var viewControllerHolder: UIViewController?
|
|
|
|
|
|
@State var index = 1
|
|
|
@State var showingAttachmentFullScreen = false
|
|
|
|
|
|
static private let cornerRadius: CGFloat = 17
|
|
|
|
|
|
var actions: [ContextMenuVC.Action]
|
|
|
var messageViewModel: MessageViewModel
|
|
|
var isMessageFailed: Bool {
|
|
|
return [.failed, .failedToSync].contains(messageViewModel.state)
|
|
|
}
|
|
|
|
|
|
var dismiss: (() -> Void)?
|
|
|
|
|
|
var body: some View {
|
|
|
NavigationView {
|
|
|
ZStack (alignment: .topLeading) {
|
|
|
if #available(iOS 14.0, *) {
|
|
|
ThemeManager.currentTheme.colorSwiftUI(for: .backgroundPrimary).ignoresSafeArea()
|
|
|
} else {
|
|
|
ThemeManager.currentTheme.colorSwiftUI(for: .backgroundPrimary)
|
|
|
}
|
|
|
|
|
|
ScrollView(.vertical, showsIndicators: false) {
|
|
|
VStack(
|
|
|
alignment: .leading,
|
|
|
spacing: 10
|
|
|
) {
|
|
|
// Message bubble snapshot
|
|
|
MessageBubble(
|
|
|
messageViewModel: messageViewModel
|
|
|
)
|
|
|
.frame(
|
|
|
maxWidth: .infinity,
|
|
|
maxHeight: .infinity,
|
|
|
alignment: .topLeading
|
|
|
)
|
|
|
.fixedSize(horizontal: true, 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,
|
|
|
hasAtLeastOneReadReceipt: messageViewModel.hasAtLeastOneReadReceipt
|
|
|
)
|
|
|
|
|
|
HStack(spacing: 6) {
|
|
|
if let image: UIImage = image?.withRenderingMode(.alwaysTemplate) {
|
|
|
Image(uiImage: image)
|
|
|
.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)
|
|
|
}
|
|
|
|
|
|
if let attachments = messageViewModel.attachments {
|
|
|
let attachment: Attachment = attachments[(index - 1 + attachments.count) % attachments.count]
|
|
|
|
|
|
ZStack(alignment: .bottomTrailing) {
|
|
|
if attachments.count > 1 {
|
|
|
// Attachment carousel view
|
|
|
SessionCarouselView_SwiftUI(
|
|
|
index: $index,
|
|
|
isOutgoing: (messageViewModel.variant == .standardOutgoing),
|
|
|
contentInfos: attachments
|
|
|
)
|
|
|
.frame(
|
|
|
maxWidth: .infinity,
|
|
|
maxHeight: .infinity,
|
|
|
alignment: .topLeading
|
|
|
)
|
|
|
} else {
|
|
|
MediaView_SwiftUI(
|
|
|
attachment: attachments[0],
|
|
|
isOutgoing: (messageViewModel.variant == .standardOutgoing),
|
|
|
cornerRadius: 0
|
|
|
)
|
|
|
.frame(
|
|
|
maxWidth: .infinity,
|
|
|
maxHeight: .infinity,
|
|
|
alignment: .topLeading
|
|
|
)
|
|
|
.aspectRatio(1, contentMode: .fit)
|
|
|
.clipShape(RoundedRectangle(cornerRadius: 15))
|
|
|
.padding(.horizontal, Values.largeSpacing)
|
|
|
}
|
|
|
|
|
|
Button {
|
|
|
self.viewControllerHolder?.present(style: .fullScreen) {
|
|
|
MediaGalleryViewModel.createDetailViewSwiftUI(
|
|
|
for: messageViewModel.threadId,
|
|
|
threadVariant: messageViewModel.threadVariant,
|
|
|
interactionId: messageViewModel.id,
|
|
|
selectedAttachmentId: attachment.id,
|
|
|
options: [ .sliderEnabled ]
|
|
|
)
|
|
|
}
|
|
|
} label: {
|
|
|
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)
|
|
|
}
|
|
|
.padding(.vertical, Values.verySmallSpacing)
|
|
|
|
|
|
// Attachment Info
|
|
|
ZStack {
|
|
|
RoundedRectangle(cornerRadius: Self.cornerRadius)
|
|
|
.fill(themeColor: .backgroundSecondary)
|
|
|
|
|
|
VStack(
|
|
|
alignment: .leading,
|
|
|
spacing: Values.mediumSpacing
|
|
|
) {
|
|
|
InfoBlock(title: "ATTACHMENT_INFO_FILE_ID".localized() + ":") {
|
|
|
Text(attachment.serverId ?? "")
|
|
|
.font(.system(size: Values.mediumFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
}
|
|
|
|
|
|
HStack(
|
|
|
alignment: .center
|
|
|
) {
|
|
|
InfoBlock(title: "ATTACHMENT_INFO_FILE_TYPE".localized() + ":") {
|
|
|
Text(attachment.contentType)
|
|
|
.font(.system(size: Values.mediumFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
}
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
InfoBlock(title: "ATTACHMENT_INFO_FILE_SIZE".localized() + ":") {
|
|
|
Text(Format.fileSize(attachment.byteCount))
|
|
|
.font(.system(size: Values.mediumFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
}
|
|
|
|
|
|
Spacer()
|
|
|
}
|
|
|
HStack(
|
|
|
alignment: .center
|
|
|
) {
|
|
|
let resolution: String = {
|
|
|
guard let width = attachment.width, let height = attachment.height else { return "N/A" }
|
|
|
return "\(width)×\(height)"
|
|
|
}()
|
|
|
InfoBlock(title: "ATTACHMENT_INFO_RESOLUTION".localized() + ":") {
|
|
|
Text(resolution)
|
|
|
.font(.system(size: Values.mediumFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
}
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
let duration: String = {
|
|
|
guard let duration = attachment.duration else { return "N/A" }
|
|
|
return floor(duration).formatted(format: .videoDuration)
|
|
|
}()
|
|
|
InfoBlock(title: "ATTACHMENT_INFO_DURATION".localized() + ":") {
|
|
|
Text(duration)
|
|
|
.font(.system(size: Values.mediumFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
}
|
|
|
|
|
|
Spacer()
|
|
|
}
|
|
|
}
|
|
|
.frame(
|
|
|
maxWidth: .infinity,
|
|
|
maxHeight: .infinity,
|
|
|
alignment: .topLeading
|
|
|
)
|
|
|
.padding(.all, Values.largeSpacing)
|
|
|
}
|
|
|
.frame(maxHeight: .infinity)
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
.padding(.vertical, Values.verySmallSpacing)
|
|
|
.padding(.horizontal, Values.largeSpacing)
|
|
|
}
|
|
|
|
|
|
// Message Info
|
|
|
ZStack {
|
|
|
RoundedRectangle(cornerRadius: Self.cornerRadius)
|
|
|
.fill(themeColor: .backgroundSecondary)
|
|
|
|
|
|
VStack(
|
|
|
alignment: .leading,
|
|
|
spacing: Values.mediumSpacing
|
|
|
) {
|
|
|
InfoBlock(title: "MESSAGE_INFO_SENT".localized() + ":") {
|
|
|
Text(messageViewModel.dateForUI.fromattedForMessageInfo)
|
|
|
.font(.system(size: Values.mediumFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
}
|
|
|
|
|
|
InfoBlock(title: "MESSAGE_INFO_RECEIVED".localized() + ":") {
|
|
|
Text(messageViewModel.receivedDateForUI.fromattedForMessageInfo)
|
|
|
.font(.system(size: Values.mediumFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
}
|
|
|
|
|
|
if isMessageFailed {
|
|
|
let failureText: String = messageViewModel.mostRecentFailureText ?? "Message failed to send"
|
|
|
InfoBlock(title: "ALERT_ERROR_TITLE".localized() + ":") {
|
|
|
Text(failureText)
|
|
|
.font(.system(size: Values.mediumFontSize))
|
|
|
.foregroundColor(themeColor: .danger)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
InfoBlock(title: "MESSAGE_INFO_FROM".localized() + ":") {
|
|
|
HStack(
|
|
|
spacing: 10
|
|
|
) {
|
|
|
let (info, additionalInfo) = ProfilePictureView.getProfilePictureInfo(
|
|
|
size: .message,
|
|
|
publicKey: messageViewModel.authorId,
|
|
|
threadVariant: .contact, // Always show the display picture in 'contact' mode
|
|
|
customImageData: nil,
|
|
|
profile: messageViewModel.profile,
|
|
|
profileIcon: (messageViewModel.isSenderOpenGroupModerator ? .crown : .none)
|
|
|
)
|
|
|
|
|
|
let size: ProfilePictureView.Size = .list
|
|
|
|
|
|
if let info: ProfilePictureView.Info = info {
|
|
|
ProfilePictureSwiftUI(
|
|
|
size: size,
|
|
|
info: info,
|
|
|
additionalInfo: additionalInfo
|
|
|
)
|
|
|
.frame(
|
|
|
width: size.viewSize,
|
|
|
height: size.viewSize,
|
|
|
alignment: .topLeading
|
|
|
)
|
|
|
}
|
|
|
|
|
|
VStack(
|
|
|
alignment: .leading,
|
|
|
spacing: Values.verySmallSpacing
|
|
|
) {
|
|
|
if !messageViewModel.authorName.isEmpty {
|
|
|
Text(messageViewModel.authorName)
|
|
|
.bold()
|
|
|
.font(.system(size: Values.mediumLargeFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
}
|
|
|
Text(messageViewModel.authorId)
|
|
|
.font(.spaceMono(size: Values.mediumFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
.frame(
|
|
|
maxWidth: .infinity,
|
|
|
maxHeight: .infinity,
|
|
|
alignment: .topLeading
|
|
|
)
|
|
|
.padding(.all, Values.largeSpacing)
|
|
|
}
|
|
|
.frame(maxHeight: .infinity)
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
.padding(.vertical, Values.verySmallSpacing)
|
|
|
.padding(.horizontal, Values.largeSpacing)
|
|
|
|
|
|
// Actions
|
|
|
if !actions.isEmpty {
|
|
|
ZStack {
|
|
|
RoundedRectangle(cornerRadius: Self.cornerRadius)
|
|
|
.fill(themeColor: .backgroundSecondary)
|
|
|
|
|
|
VStack(
|
|
|
alignment: .leading,
|
|
|
spacing: 0
|
|
|
) {
|
|
|
ForEach(
|
|
|
0...(actions.count - 1),
|
|
|
id: \.self
|
|
|
) { index in
|
|
|
let tintColor: ThemeValue = actions[index].isDestructive ? .danger : .textPrimary
|
|
|
Button(
|
|
|
action: {
|
|
|
actions[index].work()
|
|
|
dismiss?()
|
|
|
},
|
|
|
label: {
|
|
|
HStack(spacing: Values.largeSpacing) {
|
|
|
Image(uiImage: actions[index].icon!.withRenderingMode(.alwaysTemplate))
|
|
|
.resizable()
|
|
|
.scaledToFit()
|
|
|
.foregroundColor(themeColor: tintColor)
|
|
|
.frame(width: 26, height: 26)
|
|
|
Text(actions[index].title)
|
|
|
.bold()
|
|
|
.font(.system(size: Values.mediumLargeFontSize))
|
|
|
.foregroundColor(themeColor: tintColor)
|
|
|
}
|
|
|
.frame(maxWidth: .infinity, alignment: .topLeading)
|
|
|
}
|
|
|
)
|
|
|
.frame(height: 60)
|
|
|
|
|
|
if index < (actions.count - 1) {
|
|
|
Divider()
|
|
|
.foregroundColor(themeColor: .borderSeparator)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
.frame(
|
|
|
maxWidth: .infinity,
|
|
|
maxHeight: .infinity,
|
|
|
alignment: .topLeading
|
|
|
)
|
|
|
.padding(.horizontal, Values.largeSpacing)
|
|
|
}
|
|
|
.frame(maxHeight: .infinity)
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
.padding(.vertical, Values.verySmallSpacing)
|
|
|
.padding(.horizontal, Values.largeSpacing)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
struct MessageBubble: View {
|
|
|
static private let cornerRadius: CGFloat = 18
|
|
|
|
|
|
let messageViewModel: MessageViewModel
|
|
|
var bubbleBackgroundColor: ThemeValue {
|
|
|
messageViewModel.variant == .standardIncoming || messageViewModel.variant == .standardIncomingDeleted ?
|
|
|
.messageBubble_incomingBackground :
|
|
|
.messageBubble_outgoingBackground
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
}
|
|
|
|
|
|
}
|
|
|
.background(
|
|
|
RoundedRectangle(cornerRadius: Self.cornerRadius)
|
|
|
.fill(themeColor: bubbleBackgroundColor)
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
struct InfoBlock<Content>: View where Content: View {
|
|
|
let title: String
|
|
|
let content: () -> Content
|
|
|
|
|
|
private let minWidth: CGFloat = 100
|
|
|
|
|
|
var body: some View {
|
|
|
VStack(
|
|
|
alignment: .leading,
|
|
|
spacing: Values.verySmallSpacing
|
|
|
) {
|
|
|
Text(self.title)
|
|
|
.bold()
|
|
|
.font(.system(size: Values.mediumLargeFontSize))
|
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
|
self.content()
|
|
|
}
|
|
|
.frame(
|
|
|
minWidth: minWidth,
|
|
|
alignment: .leading
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
final class MessageInfoViewController: SessionHostingViewController<MessageInfoView> {
|
|
|
init(actions: [ContextMenuVC.Action], messageViewModel: MessageViewModel) {
|
|
|
let messageInfoView = MessageInfoView(
|
|
|
actions: actions,
|
|
|
messageViewModel: messageViewModel
|
|
|
)
|
|
|
|
|
|
super.init(rootView: messageInfoView)
|
|
|
rootView.dismiss = dismiss
|
|
|
}
|
|
|
|
|
|
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
}
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
super.viewDidLoad()
|
|
|
|
|
|
let customTitleFontSize = Values.largeFontSize
|
|
|
setNavBarTitle("message_info_title".localized(), customFontSize: customTitleFontSize)
|
|
|
}
|
|
|
|
|
|
func dismiss() {
|
|
|
self.navigationController?.popViewController(animated: true)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
struct MessageInfoView_Previews: PreviewProvider {
|
|
|
static var messageViewModel: MessageViewModel {
|
|
|
let result = MessageViewModel(
|
|
|
optimisticMessageId: UUID(),
|
|
|
threadId: "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg",
|
|
|
threadVariant: .contact,
|
|
|
threadHasDisappearingMessagesEnabled: false,
|
|
|
threadOpenGroupServer: nil,
|
|
|
threadOpenGroupPublicKey: nil,
|
|
|
threadContactNameInternal: "Test",
|
|
|
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
|
|
|
receivedAtTimestampMs: SnodeAPI.currentOffsetTimestampMs(),
|
|
|
authorId: "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg",
|
|
|
authorNameInternal: "Test",
|
|
|
body: "Test Message",
|
|
|
expiresStartedAtMs: nil,
|
|
|
expiresInSeconds: nil,
|
|
|
state: .failed,
|
|
|
isSenderOpenGroupModerator: false,
|
|
|
currentUserProfile: Profile.fetchOrCreateCurrentUser(),
|
|
|
quote: nil,
|
|
|
quoteAttachment: nil,
|
|
|
linkPreview: nil,
|
|
|
linkPreviewAttachment: nil,
|
|
|
attachments: nil
|
|
|
)
|
|
|
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
static var actions: [ContextMenuVC.Action] {
|
|
|
return [
|
|
|
.reply(messageViewModel, nil, using: Dependencies()),
|
|
|
.retry(messageViewModel, nil, using: Dependencies()),
|
|
|
.delete(messageViewModel, nil, using: Dependencies())
|
|
|
]
|
|
|
}
|
|
|
|
|
|
static var previews: some View {
|
|
|
MessageInfoView(
|
|
|
actions: actions,
|
|
|
messageViewModel: messageViewModel
|
|
|
)
|
|
|
}
|
|
|
}
|