You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/Session/Media Viewing & Editing/MessageInfoView.swift

544 lines
26 KiB
Swift

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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
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
if let body: String = messageViewModel.body, !body.isEmpty {
let (bubbleBackgroundColor, bubbleTextColor): (ThemeValue, ThemeValue) = (
messageViewModel.variant == .standardIncoming ||
messageViewModel.variant == .standardIncomingDeleted
) ?
(.messageBubble_incomingBackground, .messageBubble_incomingText) :
(.messageBubble_outgoingBackground, .messageBubble_outgoingText)
ZStack {
RoundedRectangle(cornerRadius: 18)
.fill(themeColor: bubbleBackgroundColor)
Text(body)
.foregroundColor(themeColor: bubbleTextColor)
.padding(
EdgeInsets(
top: 8,
leading: 16,
bottom: 8,
trailing: 16
)
)
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.fixedSize(horizontal: true, vertical: true)
.padding(
EdgeInsets(
top: 8,
leading: 30,
bottom: 4,
trailing: 30
)
)
}
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: 11))
.foregroundColor(themeColor: tintColor)
}
}
.padding(
EdgeInsets(
top: -8,
leading: 30,
bottom: 4,
trailing: 30
)
)
}
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),
shouldSupressControls: true,
cornerRadius: 0
)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.aspectRatio(1, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 15))
.padding(
EdgeInsets(
top: 0,
leading: 30,
bottom: 0,
trailing: 30
)
)
}
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(
EdgeInsets(
top: 0,
leading: 0,
bottom: 8,
trailing: 38
)
)
}
.padding(
EdgeInsets(
top: 4,
leading: 0,
bottom: 4,
trailing: 0
)
)
// Attachment Info
ZStack {
RoundedRectangle(cornerRadius: 17)
.fill(themeColor: .backgroundSecondary)
VStack(
alignment: .leading,
spacing: 16
) {
InfoBlock(title: "ATTACHMENT_INFO_FILE_ID".localized() + ":") {
Text(attachment.serverId ?? "")
.font(.system(size: 16))
.foregroundColor(themeColor: .textPrimary)
}
HStack(
alignment: .center
) {
InfoBlock(title: "ATTACHMENT_INFO_FILE_TYPE".localized() + ":") {
Text(attachment.contentType)
.font(.system(size: 16))
.foregroundColor(themeColor: .textPrimary)
}
Spacer()
InfoBlock(title: "ATTACHMENT_INFO_FILE_SIZE".localized() + ":") {
Text(Format.fileSize(attachment.byteCount))
.font(.system(size: 16))
.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: 16))
.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: 16))
.foregroundColor(themeColor: .textPrimary)
}
Spacer()
}
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(
EdgeInsets(
top: 24,
leading: 24,
bottom: 24,
trailing: 24
)
)
}
.frame(maxHeight: .infinity)
.fixedSize(horizontal: false, vertical: true)
.padding(
EdgeInsets(
top: 4,
leading: 30,
bottom: 4,
trailing: 30
)
)
}
// Message Info
ZStack {
RoundedRectangle(cornerRadius: 17)
.fill(themeColor: .backgroundSecondary)
VStack(
alignment: .leading,
spacing: 16
) {
InfoBlock(title: "MESSAGE_INFO_SENT".localized() + ":") {
Text(messageViewModel.dateForUI.fromattedForMessageInfo)
.font(.system(size: 16))
.foregroundColor(themeColor: .textPrimary)
}
InfoBlock(title: "MESSAGE_INFO_RECEIVED".localized() + ":") {
Text(messageViewModel.receivedDateForUI.fromattedForMessageInfo)
.font(.system(size: 16))
.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: 16))
.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: 4
) {
if !messageViewModel.authorName.isEmpty {
Text(messageViewModel.authorName)
.bold()
.font(.system(size: 18))
.foregroundColor(themeColor: .textPrimary)
}
Text(messageViewModel.authorId)
.font(.spaceMono(size: 16))
.foregroundColor(themeColor: .textPrimary)
}
}
}
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(
EdgeInsets(
top: 24,
leading: 24,
bottom: 24,
trailing: 24
)
)
}
.frame(maxHeight: .infinity)
.fixedSize(horizontal: false, vertical: true)
.padding(
EdgeInsets(
top: 4,
leading: 30,
bottom: 4,
trailing: 30
)
)
// Actions
if !actions.isEmpty {
ZStack {
RoundedRectangle(cornerRadius: 17)
.fill(themeColor: .backgroundSecondary)
VStack(
alignment: .leading,
spacing: 0
) {
ForEach(
0...(actions.count - 1),
id: \.self
) { index in
let tintColor: ThemeValue = actions[index].themeColor
Button(
action: {
actions[index].work()
dismiss?()
},
label: {
HStack(spacing: 24) {
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: 18))
.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(
EdgeInsets(
top: 0,
leading: 24,
bottom: 0,
trailing: 24
)
)
}
.frame(maxHeight: .infinity)
.fixedSize(horizontal: false, vertical: true)
.padding(
EdgeInsets(
top: 4,
leading: 30,
bottom: 4,
trailing: 30
)
)
}
}
}
}
}
}
}
struct InfoBlock<Content>: View where Content: View {
let title: String
let content: () -> Content
var body: some View {
VStack(
alignment: .leading,
spacing: 4
) {
Text(self.title)
.bold()
.font(.system(size: 18))
.foregroundColor(themeColor: .textPrimary)
self.content()
}
.frame(
minWidth: 100,
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.content.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,
threadExpirationType: nil,
threadExpirationTimer: nil,
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
)
}
}