diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 72e17b281..44aceb456 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -109,6 +109,12 @@ 7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; }; 7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; }; 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; }; + 7B2561C22978B307005C086C /* MediaInfoVC+MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */; }; + 7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */; }; + 7B3A392E2977791E002FE4AC /* MediaInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */; }; + 7B3A3930297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */; }; + 7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */; }; + 7B3A3934298882D6002FE4AC /* SessionCarouselViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */; }; 7B2E985829AC227C001792D7 /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */; }; 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; @@ -1179,7 +1185,13 @@ 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.swift; sourceTree = ""; }; 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = ""; }; 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = ""; }; + 7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaInfoView.swift"; sourceTree = ""; }; + 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCarouselView+Info.swift"; sourceTree = ""; }; 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; + 7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInfoVC.swift; sourceTree = ""; }; + 7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaPreviewView.swift"; sourceTree = ""; }; + 7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselView.swift; sourceTree = ""; }; + 7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselViewDelegate.swift; sourceTree = ""; }; 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = ""; }; 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; @@ -2588,6 +2600,9 @@ FD52090828B59411006098F6 /* ScreenLockUI.swift */, FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */, FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */, + 7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */, + 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */, + 7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */, ); path = Shared; sourceTree = ""; @@ -2991,6 +3006,9 @@ 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */, 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */, 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */, + 7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */, + 7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */, + 7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */, ); path = "Media Viewing & Editing"; sourceTree = ""; @@ -5588,6 +5606,7 @@ buildActionMask = 2147483647; files = ( FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */, + 7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */, FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */, FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */, B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */, @@ -5602,6 +5621,7 @@ 7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */, B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */, FD37E9D928A230F2003AE748 /* TraitObservingWindow.swift in Sources */, + 7B3A3930297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift in Sources */, B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */, FD848B8F283EF2A8000E298B /* UIScrollView+Utilities.swift in Sources */, B879D449247E1BE300DB3608 /* PathVC.swift in Sources */, @@ -5645,6 +5665,7 @@ FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */, B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */, FD71163A28E2C53700B47552 /* SessionAvatarCell.swift in Sources */, + 7B3A392E2977791E002FE4AC /* MediaInfoVC.swift in Sources */, 7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */, B835247925C38D880089A44F /* MessageCell.swift in Sources */, B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */, @@ -5704,6 +5725,7 @@ FD71164228E2C85A00B47552 /* TransitionType.swift in Sources */, FD848B9828422F1A000E298B /* Date+Utilities.swift in Sources */, FD37E9DB28A244E9003AE748 /* ThemePreviewView.swift in Sources */, + 7B3A3934298882D6002FE4AC /* SessionCarouselViewDelegate.swift in Sources */, B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */, 45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */, 7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */, @@ -5752,6 +5774,7 @@ FD39352C28F382920084DADA /* VersionFooterView.swift in Sources */, 7B9F71D22852EEE2006DFE7B /* Emoji+SkinTones.swift in Sources */, 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */, + 7B2561C22978B307005C086C /* MediaInfoVC+MediaInfoView.swift in Sources */, B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */, 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */, B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */, @@ -5773,6 +5796,7 @@ FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */, FD71164A28E3EA5B00B47552 /* DismissType.swift in Sources */, C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */, + 7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */, FD37E9CC28A1E578003AE748 /* AppearanceViewController.swift in Sources */, B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */, C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */, diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index a66dcfef3..a21818707 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -35,6 +35,14 @@ extension ContextMenuVC { // MARK: - Actions + static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { + return Action( + icon: UIImage(named: "ic_info"), + title: "context_menu_info".localized(), + accessibilityLabel: "Message info" + ) { delegate?.info(cellViewModel) } + } + static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(systemName: "arrow.triangle.2.circlepath"), @@ -207,6 +215,8 @@ extension ContextMenuVC { return !currentThreadIsMessageRequest }() + let shouldShowInfo: Bool = (cellViewModel.attachments?.isEmpty == false) + let generatedActions: [Action] = [ (canRetry ? Action.retry(cellViewModel, delegate) : nil), (canReply ? Action.reply(cellViewModel, delegate) : nil), @@ -216,6 +226,7 @@ extension ContextMenuVC { (canDelete ? Action.delete(cellViewModel, delegate) : nil), (canBan ? Action.ban(cellViewModel, delegate) : nil), (canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate) : nil), + (shouldShowInfo ? Action.info(cellViewModel, delegate) : nil), ] .appending(contentsOf: (shouldShowEmojiActions ? recentEmojis : []).map { Action.react(cellViewModel, $0, delegate) }) .appending(Action.emojiPlusButton(cellViewModel, delegate)) @@ -230,6 +241,7 @@ extension ContextMenuVC { // MARK: - Delegate protocol ContextMenuActionDelegate { + func info(_ cellViewModel: MessageViewModel) func retry(_ cellViewModel: MessageViewModel) func reply(_ cellViewModel: MessageViewModel) func copy(_ cellViewModel: MessageViewModel) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index d68851588..3c9d9aab0 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -164,7 +164,9 @@ final class ContextMenuVC: UIViewController { let menuStackView = UIStackView( arrangedSubviews: actions .filter { !$0.isEmojiAction && !$0.isEmojiPlus && !$0.isDismissAction } - .map { action -> ActionView in ActionView(for: action, dismiss: snDismiss) } + .map { action -> ActionView in + ActionView(for: action, dismiss: snDismiss) + } ) menuStackView.axis = .vertical menuBackgroundView.addSubview(menuStackView) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index b6b802eb9..2d98029d5 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1600,6 +1600,17 @@ extension ConversationVC: // MARK: - ContextMenuActionDelegate + func info(_ cellViewModel: MessageViewModel) { + let mediaInfoVC = MediaInfoVC( + attachments: (cellViewModel.attachments ?? []), + isOutgoing: (cellViewModel.variant == .standardOutgoing), + threadId: self.viewModel.threadData.threadId, + threadVariant: self.viewModel.threadData.threadVariant, + interactionId: cellViewModel.id + ) + navigationController?.pushViewController(mediaInfoVC, animated: true) + } + func retry(_ cellViewModel: MessageViewModel) { Storage.shared.writeAsync { [weak self] db in guard diff --git a/Session/Conversations/Message Cells/Content Views/DocumentView.swift b/Session/Conversations/Message Cells/Content Views/DocumentView.swift index cdcfe5fed..88d2dc07c 100644 --- a/Session/Conversations/Message Cells/Content Views/DocumentView.swift +++ b/Session/Conversations/Message Cells/Content Views/DocumentView.swift @@ -46,7 +46,7 @@ final class DocumentView: UIView { // Size label let sizeLabel = UILabel() sizeLabel.font = .systemFont(ofSize: Values.verySmallFontSize) - sizeLabel.text = OWSFormat.formatFileSize(UInt(attachment.byteCount)) + sizeLabel.text = OWSFormat.formatFileSize(attachment.byteCount) sizeLabel.themeTextColor = textColor sizeLabel.lineBreakMode = .byTruncatingTail diff --git a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift index 846e0aa72..a4cdf7e67 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift @@ -29,7 +29,7 @@ public class MediaAlbumView: UIStackView { mediaCache: mediaCache, attachment: $0, isOutgoing: isOutgoing, - maxMessageWidth: maxMessageWidth + cornerRadius: VisibleMessageCell.largeCornerRadius ) } diff --git a/Session/Conversations/Message Cells/Content Views/MediaView.swift b/Session/Conversations/Message Cells/Content Views/MediaView.swift index 301818a8b..507b72917 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaView.swift @@ -16,10 +16,9 @@ public class MediaView: UIView { // MARK: - - private let mediaCache: NSCache + private let mediaCache: NSCache? public let attachment: Attachment private let isOutgoing: Bool - private let maxMessageWidth: CGFloat private var loadBlock: (() -> Void)? private var unloadBlock: (() -> Void)? @@ -46,22 +45,21 @@ public class MediaView: UIView { // MARK: - Initializers public required init( - mediaCache: NSCache, + mediaCache: NSCache? = nil, attachment: Attachment, isOutgoing: Bool, - maxMessageWidth: CGFloat + cornerRadius: CGFloat ) { self.mediaCache = mediaCache self.attachment = attachment self.isOutgoing = isOutgoing - self.maxMessageWidth = maxMessageWidth super.init(frame: .zero) themeBackgroundColor = .backgroundSecondary clipsToBounds = true layer.masksToBounds = true - layer.cornerRadius = VisibleMessageCell.largeCornerRadius + layer.cornerRadius = cornerRadius createContents() } @@ -396,7 +394,7 @@ public class MediaView: UIView { applyMediaBlock(media) - self?.mediaCache.setObject(media, forKey: cacheKey as NSString) + self?.mediaCache?.setObject(media, forKey: cacheKey as NSString) self?.loadState.mutate { $0 = .loaded } } @@ -405,7 +403,7 @@ public class MediaView: UIView { return } - if let media: AnyObject = self.mediaCache.object(forKey: cacheKey as NSString) { + if let media: AnyObject = self.mediaCache?.object(forKey: cacheKey as NSString) { Logger.verbose("media cache hit") guard Thread.isMainThread else { diff --git a/Session/Media Viewing & Editing/MediaInfoVC+MediaInfoView.swift b/Session/Media Viewing & Editing/MediaInfoVC+MediaInfoView.swift new file mode 100644 index 000000000..25c0f5774 --- /dev/null +++ b/Session/Media Viewing & Editing/MediaInfoVC+MediaInfoView.swift @@ -0,0 +1,191 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit +import SessionUtilitiesKit + +extension MediaInfoVC { + final class MediaInfoView: UIView { + private static let cornerRadius: CGFloat = 12 + + private var attachment: Attachment? + private let width: CGFloat = MediaInfoVC.mediaSize - 2 * MediaInfoVC.arrowSize.width + + // MARK: - UI + + private lazy var fileIdLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.themeTextColor = .textPrimary + + return result + }() + + private lazy var fileTypeLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.themeTextColor = .textPrimary + + return result + }() + + private lazy var fileSizeLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.themeTextColor = .textPrimary + + return result + }() + + private lazy var resolutionLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.themeTextColor = .textPrimary + + return result + }() + + private lazy var durationLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.themeTextColor = .textPrimary + + return result + }() + + // MARK: - Lifecycle + + init(attachment: Attachment?) { + self.attachment = attachment + + super.init(frame: CGRect.zero) + self.accessibilityLabel = "Media info" + setUpViewHierarchy() + update(attachment: attachment) + } + + override init(frame: CGRect) { + preconditionFailure("Use init(attachment:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(attachment:) instead.") + } + + private func setUpViewHierarchy() { + let backgroundView: UIView = UIView() + backgroundView.clipsToBounds = true + backgroundView.themeBackgroundColor = .contextMenu_background + backgroundView.layer.cornerRadius = Self.cornerRadius + addSubview(backgroundView) + backgroundView.pin(to: self) + + let container: UIView = UIView() + container.set(.width, to: self.width) + + // File ID + let fileIdTitleLabel: UILabel = { + let result = UILabel() + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.text = "ATTACHMENT_INFO_FILE_ID".localized() + ":" + result.themeTextColor = .textPrimary + + return result + }() + let fileIdContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ fileIdTitleLabel, fileIdLabel ]) + fileIdContainerStackView.axis = .vertical + fileIdContainerStackView.spacing = 6 + container.addSubview(fileIdContainerStackView) + fileIdContainerStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: container) + + // File Type + let fileTypeTitleLabel: UILabel = { + let result = UILabel() + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.text = "ATTACHMENT_INFO_FILE_TYPE".localized() + ":" + result.themeTextColor = .textPrimary + + return result + }() + let fileTypeContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ fileTypeTitleLabel, fileTypeLabel ]) + fileTypeContainerStackView.axis = .vertical + fileTypeContainerStackView.spacing = 6 + container.addSubview(fileTypeContainerStackView) + fileTypeContainerStackView.pin(.leading, to: .leading, of: container) + fileTypeContainerStackView.pin(.top, to: .bottom, of: fileIdContainerStackView, withInset: Values.largeSpacing) + + // File Size + let fileSizeTitleLabel: UILabel = { + let result = UILabel() + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.text = "ATTACHMENT_INFO_FILE_SIZE".localized() + ":" + result.themeTextColor = .textPrimary + + return result + }() + let fileSizeContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ fileSizeTitleLabel, fileSizeLabel ]) + fileSizeContainerStackView.axis = .vertical + fileSizeContainerStackView.spacing = 6 + container.addSubview(fileSizeContainerStackView) + fileSizeContainerStackView.pin(.trailing, to: .trailing, of: container) + fileSizeContainerStackView.pin(.top, to: .bottom, of: fileIdContainerStackView, withInset: Values.largeSpacing) + fileSizeContainerStackView.set(.width, to: 90) + + // Resolution + let resolutionTitleLabel: UILabel = { + let result = UILabel() + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.text = "ATTACHMENT_INFO_RESOLUTION".localized() + ":" + result.themeTextColor = .textPrimary + + return result + }() + let resolutionContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ resolutionTitleLabel, resolutionLabel ]) + resolutionContainerStackView.axis = .vertical + resolutionContainerStackView.spacing = 6 + container.addSubview(resolutionContainerStackView) + resolutionContainerStackView.pin(.leading, to: .leading, of: container) + resolutionContainerStackView.pin(.top, to: .bottom, of: fileTypeContainerStackView, withInset: Values.largeSpacing) + + // Duration + let durationTitleLabel: UILabel = { + let result = UILabel() + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.text = "ATTACHMENT_INFO_DURATION".localized() + ":" + result.themeTextColor = .textPrimary + + return result + }() + let durationContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ durationTitleLabel, durationLabel ]) + durationContainerStackView.axis = .vertical + durationContainerStackView.spacing = 6 + container.addSubview(durationContainerStackView) + durationContainerStackView.pin(.trailing, to: .trailing, of: container) + durationContainerStackView.pin(.top, to: .bottom, of: fileSizeContainerStackView, withInset: Values.largeSpacing) + durationContainerStackView.set(.width, to: 90) + container.pin(.bottom, to: .bottom, of: durationContainerStackView) + + backgroundView.addSubview(container) + container.pin(to: backgroundView, withInset: Values.largeSpacing) + } + + // MARK: - Interaction + public func update(attachment: Attachment?) { + guard let attachment: Attachment = attachment else { return } + + self.attachment = attachment + + fileIdLabel.text = attachment.serverId + fileTypeLabel.text = attachment.contentType + fileSizeLabel.text = OWSFormat.formatFileSize(attachment.byteCount) + resolutionLabel.text = { + guard let width = attachment.width, let height = attachment.height else { return "N/A" } + return "\(width)×\(height)" + }() + durationLabel.text = { + guard let duration = attachment.duration else { return "N/A" } + return floor(duration).formatted(format: .videoDuration) + }() + } + } +} diff --git a/Session/Media Viewing & Editing/MediaInfoVC+MediaPreviewView.swift b/Session/Media Viewing & Editing/MediaInfoVC+MediaPreviewView.swift new file mode 100644 index 000000000..2ca703e6e --- /dev/null +++ b/Session/Media Viewing & Editing/MediaInfoVC+MediaPreviewView.swift @@ -0,0 +1,62 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit +import SessionUtilitiesKit + +extension MediaInfoVC { + final class MediaPreviewView: UIView { + private static let cornerRadius: CGFloat = 8 + + private let attachment: Attachment + private let isOutgoing: Bool + + // MARK: - UI + + private lazy var mediaView: MediaView = { + let result: MediaView = MediaView.init( + attachment: attachment, + isOutgoing: isOutgoing, + cornerRadius: 0 + ) + + return result + }() + + // MARK: - Lifecycle + + init(attachment: Attachment, isOutgoing: Bool) { + self.attachment = attachment + self.isOutgoing = isOutgoing + + super.init(frame: CGRect.zero) + self.accessibilityLabel = "Media info" + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(attachment:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(attachment:) instead.") + } + + private func setUpViewHierarchy() { + set(.width, to: MediaInfoVC.mediaSize) + set(.height, to: MediaInfoVC.mediaSize) + + addSubview(mediaView) + mediaView.pin(to: self) + + mediaView.loadMedia() + } + + // MARK: - Copy + + /// This function is used to make sure the carousel view contains this class can loop infinitely + func copyView() -> MediaPreviewView { + return MediaPreviewView(attachment: self.attachment, isOutgoing: self.isOutgoing) + } + } +} diff --git a/Session/Media Viewing & Editing/MediaInfoVC.swift b/Session/Media Viewing & Editing/MediaInfoVC.swift new file mode 100644 index 000000000..26711c83b --- /dev/null +++ b/Session/Media Viewing & Editing/MediaInfoVC.swift @@ -0,0 +1,149 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit +import SessionUtilitiesKit + +final class MediaInfoVC: BaseVC, SessionCarouselViewDelegate { + internal static let mediaSize: CGFloat = UIScreen.main.bounds.width - 2 * Values.veryLargeSpacing + internal static let arrowSize: CGSize = CGSize(width: 20, height: 30) + + private let attachments: [Attachment] + private let isOutgoing: Bool + private let threadId: String + private let threadVariant: SessionThread.Variant + private let interactionId: Int64 + + private var currentPage: Int = 0 + + // MARK: - UI + private lazy var mediaInfoView: MediaInfoView = MediaInfoView(attachment: nil) + private lazy var mediaCarouselView: SessionCarouselView = { + let slices: [MediaPreviewView] = self.attachments.map { + MediaPreviewView( + attachment: $0, + isOutgoing: self.isOutgoing + ) + } + let result: SessionCarouselView = SessionCarouselView( + info: SessionCarouselView.Info( + slices: slices, + copyOfFirstSlice: slices.first?.copyView(), + copyOfLastSlice: slices.last?.copyView(), + sliceSize: CGSize( + width: Self.mediaSize, + height: Self.mediaSize + ), + shouldShowPageControl: true, + pageControlStyle: SessionCarouselView.PageControlStyle( + size: .medium, + backgroundColor: .init(white: 0, alpha: 0.4), + bottomInset: Values.mediumSpacing + ), + shouldShowArrows: true, + arrowsSize: Self.arrowSize, + cornerRadius: 8 + ) + ) + result.set(.height, to: Self.mediaSize) + result.delegate = self + + return result + }() + + private lazy var fullScreenButton: UIButton = { + let result: UIButton = UIButton(type: .custom) + result.setImage( + UIImage(systemName: "arrow.up.left.and.arrow.down.right")? + .withRenderingMode(.alwaysTemplate), + for: .normal + ) + result.themeTintColor = .textPrimary + result.backgroundColor = .init(white: 0, alpha: 0.4) + result.layer.cornerRadius = 14 + result.set(.width, to: 28) + result.set(.height, to: 28) + result.addTarget(self, action: #selector(showMediaFullScreen), for: .touchUpInside) + + return result + }() + + // MARK: - Initialization + + init( + attachments: [Attachment], + isOutgoing: Bool, + threadId: String, + threadVariant: SessionThread.Variant, + interactionId: Int64 + ) { + self.threadId = threadId + self.threadVariant = threadVariant + self.interactionId = interactionId + self.isOutgoing = isOutgoing + self.attachments = attachments + super.init(nibName: nil, bundle: nil) + } + + override init(nibName: String?, bundle: Bundle?) { + preconditionFailure("Use init(attachments:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(attachments:) instead.") + } + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + ViewControllerUtilities.setUpDefaultSessionStyle( + for: self, + title: "message_info_title".localized(), + hasCustomBackButton: false + ) + + let mediaStackView: UIStackView = UIStackView() + mediaStackView.axis = .horizontal + + mediaInfoView.update(attachment: attachments[0]) + + mediaCarouselView.addSubview(fullScreenButton) + fullScreenButton.pin(.trailing, to: .trailing, of: mediaCarouselView, withInset: -(Values.smallSpacing + Values.veryLargeSpacing)) + fullScreenButton.pin(.bottom, to: .bottom, of: mediaCarouselView, withInset: -Values.smallSpacing) + + let stackView: UIStackView = UIStackView(arrangedSubviews: [ mediaCarouselView, mediaInfoView ]) + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = Values.largeSpacing + + self.view.addSubview(stackView) + stackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self.view) + stackView.pin(.top, to: .top, of: self.view, withInset: Values.veryLargeSpacing) + } + + // MARK: - Interaction + + @objc func showMediaFullScreen() { + let attachment = self.attachments[self.currentPage] + let viewController: UIViewController? = MediaGalleryViewModel.createDetailViewController( + for: self.threadId, + threadVariant: self.threadVariant, + interactionId: self.interactionId, + selectedAttachmentId: attachment.id, + options: [ .sliderEnabled ] + ) + if let viewController: UIViewController = viewController { + viewController.transitioningDelegate = nil + self.present(viewController, animated: true) + } + } + + // MARK: - SessionCarouselViewDelegate + + func carouselViewDidScrollToNewSlice(currentPage: Int) { + self.currentPage = currentPage + mediaInfoView.update(attachment: attachments[currentPage]) + } +} diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index f5007fbe3..91de95f61 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Nur für mich löschen"; "delete_message_for_everyone" = "Für jeden löschen"; "delete_message_for_me_and_recipient" = "Für mich und %@ löschen"; +"context_menu_info" = "Info"; "context_menu_reply" = "Antworten"; "context_menu_save" = "Speichern"; "context_menu_ban_user" = "Nutzer sperren"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index da74d3933..744b6f641 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Delete just for me"; "delete_message_for_everyone" = "Delete for everyone"; "delete_message_for_me_and_recipient" = "Delete for me and %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Reply"; "context_menu_save" = "Save"; "context_menu_ban_user" = "Ban User"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 46107d2bc..0e0b642bf 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Eliminar solo para mí"; "delete_message_for_everyone" = "Eliminar para todos"; "delete_message_for_me_and_recipient" = "Eliminar para mí y para %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Responder"; "context_menu_save" = "Guardar"; "context_menu_ban_user" = "Banear Usuario"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index b5cc4f0f8..d54a2f83e 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "حذف برای من"; "delete_message_for_everyone" = "حذف برای همه"; "delete_message_for_me_and_recipient" = "حذف برای من و %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "پاسخ"; "context_menu_save" = "ذخیره"; "context_menu_ban_user" = "مسدود کردن کاربر"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 666585405..b5d2ed518 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Poista vain minun nähtäväksi"; "delete_message_for_everyone" = "Poista kaikkien näkyviltä"; "delete_message_for_me_and_recipient" = "Poista minulta ja vastaanottajalta"; +"context_menu_info" = "Info"; "context_menu_reply" = "Vastaa"; "context_menu_save" = "Tallenna"; "context_menu_ban_user" = "Estä Käyttäjä"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 6625f503b..1ef9e1825 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Supprimer pour moi uniquement"; "delete_message_for_everyone" = "Supprimer pour tout le monde"; "delete_message_for_me_and_recipient" = "Supprimer pour moi et %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Répondre"; "context_menu_save" = "Enregistrer"; "context_menu_ban_user" = "Bannir l'utilisateur"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index e1cdaac55..02573d3b8 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Delete just for me"; "delete_message_for_everyone" = "Delete for everyone"; "delete_message_for_me_and_recipient" = "Delete for me and %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Reply"; "context_menu_save" = "Save"; "context_menu_ban_user" = "Ban User"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 77cafa902..0facdab28 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Izbriši samo za mene"; "delete_message_for_everyone" = "Izbriši za sve"; "delete_message_for_me_and_recipient" = "Izbriši za mene i %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Odgovori"; "context_menu_save" = "Spremi"; "context_menu_ban_user" = "Zabrani korisnik"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 1f23902a3..20c6c9c0b 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Delete just for me"; "delete_message_for_everyone" = "Delete for everyone"; "delete_message_for_me_and_recipient" = "Delete for me and %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Reply"; "context_menu_save" = "Save"; "context_menu_ban_user" = "Ban User"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index c824b802c..c570b1477 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Elimina solo per me"; "delete_message_for_everyone" = "Elimina per tutti"; "delete_message_for_me_and_recipient" = "Elimina per me e %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Rispondi"; "context_menu_save" = "Salva"; "context_menu_ban_user" = "Banna utente"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 75cb1e6bf..12a9dcb22 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "自分の端末から削除"; "delete_message_for_everyone" = "全員の端末から削除"; "delete_message_for_me_and_recipient" = "自分と %@ の端末から削除する"; +"context_menu_info" = "Info"; "context_menu_reply" = "返信"; "context_menu_save" = "保存"; "context_menu_ban_user" = "ユーザーをBAN"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 4c9b31f6e..22dc0471d 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Verwijder alleen voor mij"; "delete_message_for_everyone" = "Verwijder voor iedereen"; "delete_message_for_me_and_recipient" = "Verwijderen voor mij en %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Antwoord"; "context_menu_save" = "Opslaan"; "context_menu_ban_user" = "Gebruiker verbannen"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 35d1f7a04..918b83ece 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -366,11 +366,12 @@ "delete_message_for_me" = "Usuń tylko dla mnie"; "delete_message_for_everyone" = "Usuń dla wszystkich"; "delete_message_for_me_and_recipient" = "Usuń dla mnie i %@"; -"context_menu_ban_user_error_alert_message" = "Unable to ban user"; +"context_menu_info" = "Info"; "context_menu_reply" = "Odpowiedz"; "context_menu_save" = "Zapisz"; "context_menu_ban_user" = "Zbanuj użytkownika"; "context_menu_ban_and_delete_all" = "Zbanuj i usuń wszystko"; +"context_menu_ban_user_error_alert_message" = "Unable to ban user"; "accessibility_expanding_attachments_button" = "Dodaj załączniki"; "accessibility_gif_button" = "Gif"; "accessibility_document_button" = "Dokument"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index c351ad411..ee83738af 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Apagar para mim"; "delete_message_for_everyone" = "Apagar para todos"; "delete_message_for_me_and_recipient" = "Apagar para mim e para %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Responder"; "context_menu_save" = "Salvar"; "context_menu_ban_user" = "Banir Usuário"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 26a64e4a4..f38d284ad 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Удалить только для меня"; "delete_message_for_everyone" = "Удалить для всех"; "delete_message_for_me_and_recipient" = "Удалить для меня и %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Ответить"; "context_menu_save" = "Сохранить"; "context_menu_ban_user" = "Заблокировать пользователя"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 349867b2c..e7642ad63 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Delete just for me"; "delete_message_for_everyone" = "Delete for everyone"; "delete_message_for_me_and_recipient" = "Delete for me and %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "පිළිතුරු"; "context_menu_save" = "සුරකින්න"; "context_menu_ban_user" = "Ban User"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 71cbe0cd5..db623fbb2 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Vymazať len u mňa"; "delete_message_for_everyone" = "Vymazať u všetkých"; "delete_message_for_me_and_recipient" = "Vymazať pre mňa a %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Odpovedať"; "context_menu_save" = "Uložiť"; "context_menu_ban_user" = "Zablokovanie používateľa"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index f0a1f421a..28bda12ed 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Delete just for me"; "delete_message_for_everyone" = "Delete for everyone"; "delete_message_for_me_and_recipient" = "Delete for me and %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Reply"; "context_menu_save" = "Spara"; "context_menu_ban_user" = "Ban User"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index e32ffabad..0b000b1c2 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Delete just for me"; "delete_message_for_everyone" = "Delete for everyone"; "delete_message_for_me_and_recipient" = "Delete for me and %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Reply"; "context_menu_save" = "Save"; "context_menu_ban_user" = "Ban User"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 049e0782d..eefe6cd65 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "Delete just for me"; "delete_message_for_everyone" = "Delete for everyone"; "delete_message_for_me_and_recipient" = "Delete for me and %@"; +"context_menu_info" = "Info"; "context_menu_reply" = "Reply"; "context_menu_save" = "Save"; "context_menu_ban_user" = "Ban User"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 2eb03bd1a..ded529a9e 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "只為我自己刪除"; "delete_message_for_everyone" = "從所有人的裝置上刪除"; "delete_message_for_me_and_recipient" = "為我和 %@ 刪除"; +"context_menu_info" = "Info"; "context_menu_reply" = "回覆"; "context_menu_save" = "儲存"; "context_menu_ban_user" = "封鎖用戶"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 9e1b96ac2..4627f4fd9 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -366,6 +366,7 @@ "delete_message_for_me" = "仅为我删除"; "delete_message_for_everyone" = "为所有人删除"; "delete_message_for_me_and_recipient" = "为我和 %@ 删除"; +"context_menu_info" = "Info"; "context_menu_reply" = "回复"; "context_menu_save" = "保存"; "context_menu_ban_user" = "封禁用户"; @@ -592,6 +593,14 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_INFO_SENT" = "Sent"; +"MESSAGE_INFO_RECEIVED" = "Received"; +"MESSAGE_INFO_FROM" = "From"; +"ATTACHMENT_INFO_FILE_ID" = "File ID"; +"ATTACHMENT_INFO_FILE_TYPE" = "File Type"; +"ATTACHMENT_INFO_FILE_SIZE" = "File Size"; +"ATTACHMENT_INFO_RESOLUTION" = "Resolution"; +"ATTACHMENT_INFO_DURATION" = "Duration"; "MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; "MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; "MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; @@ -601,6 +610,7 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"message_info_title" = "Message Info"; "mute_button_text" = "Mute"; "unmute_button_text" = "Unmute"; "mark_read_button_text" = "Mark read"; diff --git a/Session/Shared/SessionCarouselView+Info.swift b/Session/Shared/SessionCarouselView+Info.swift new file mode 100644 index 000000000..62e7427c4 --- /dev/null +++ b/Session/Shared/SessionCarouselView+Info.swift @@ -0,0 +1,72 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit +import SessionUtilitiesKit + +extension SessionCarouselView { + public struct Info { + let slices: [UIView] + let copyOfFirstSlice: UIView? + let copyOfLastSlice: UIView? + let sliceSize: CGSize + let sliceCount: Int + let shouldShowPageControl: Bool + let pageControlStyle: PageControlStyle + let shouldShowArrows: Bool + let arrowsSize: CGSize + let cornerRadius: CGFloat + + // MARK: - Initialization + + init( + slices: [UIView] = [], + copyOfFirstSlice: UIView? = nil, + copyOfLastSlice: UIView? = nil, + sliceSize: CGSize = .zero, + shouldShowPageControl: Bool = true, + pageControlStyle: PageControlStyle, + shouldShowArrows: Bool = true, + arrowsSize: CGSize = .zero, + cornerRadius: CGFloat = 0 + ) { + self.slices = slices + self.copyOfFirstSlice = copyOfFirstSlice + self.copyOfLastSlice = copyOfLastSlice + self.sliceSize = sliceSize + self.sliceCount = slices.count + self.shouldShowPageControl = shouldShowPageControl && (self.sliceCount > 1) + self.pageControlStyle = pageControlStyle + self.shouldShowArrows = shouldShowArrows && (self.sliceCount > 1) + self.arrowsSize = arrowsSize + self.cornerRadius = cornerRadius + } + } + + public struct PageControlStyle { + enum DotSize: CGFloat { + case mini = 0.5 + case medium = 0.8 + case original = 1 + } + + let height: CGFloat? + let size: DotSize + let backgroundColor: UIColor + let bottomInset: CGFloat + + // MARK: - Initialization + + init( + height: CGFloat? = nil, + size: DotSize = .original, + backgroundColor: UIColor = .clear, + bottomInset: CGFloat = 0 + ) { + self.height = height + self.size = size + self.backgroundColor = backgroundColor + self.bottomInset = bottomInset + } + } +} diff --git a/Session/Shared/SessionCarouselView.swift b/Session/Shared/SessionCarouselView.swift new file mode 100644 index 000000000..6111f6df0 --- /dev/null +++ b/Session/Shared/SessionCarouselView.swift @@ -0,0 +1,202 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit +import SessionUtilitiesKit + +final class SessionCarouselView: UIView, UIScrollViewDelegate { + private let slicesForLoop: [UIView] + private let info: SessionCarouselView.Info + var delegate: SessionCarouselViewDelegate? + + // MARK: - UI + private lazy var scrollView: UIScrollView = { + let result: UIScrollView = UIScrollView() + result.delegate = self + result.isPagingEnabled = true + result.showsHorizontalScrollIndicator = false + result.showsVerticalScrollIndicator = false + result.contentSize = CGSize( + width: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count), + height: self.info.sliceSize.height + ) + result.layer.cornerRadius = self.info.cornerRadius + result.layer.masksToBounds = true + + return result + }() + + private lazy var pageControl: UIPageControl = { + let result: UIPageControl = UIPageControl() + result.numberOfPages = self.info.sliceCount + result.currentPage = 0 + result.isHidden = !self.info.shouldShowPageControl + result.transform = CGAffineTransform( + scaleX: self.info.pageControlStyle.size.rawValue, + y: self.info.pageControlStyle.size.rawValue + ) + + return result + }() + + private lazy var arrowLeft: UIButton = { + let result = UIButton(type: .custom) + result.setImage(UIImage(systemName: "chevron.left")?.withRenderingMode(.alwaysTemplate), for: .normal) + result.addTarget(self, action: #selector(scrollToPreviousSlice), for: .touchUpInside) + result.themeTintColor = .textPrimary + result.set(.width, to: self.info.arrowsSize.width) + result.set(.height, to: self.info.arrowsSize.height) + result.isHidden = !self.info.shouldShowArrows + + return result + }() + + private lazy var arrowRight: UIButton = { + let result = UIButton(type: .custom) + result.setImage(UIImage(systemName: "chevron.right")?.withRenderingMode(.alwaysTemplate), for: .normal) + result.addTarget(self, action: #selector(scrollToNextSlice), for: .touchUpInside) + result.themeTintColor = .textPrimary + result.set(.width, to: self.info.arrowsSize.width) + result.set(.height, to: self.info.arrowsSize.height) + result.isHidden = !self.info.shouldShowArrows + + return result + }() + + // MARK: - Lifecycle + + init(info: SessionCarouselView.Info) { + self.info = info + if self.info.sliceCount > 1, + let copyOfFirstSlice: UIView = self.info.copyOfFirstSlice, + let copyOfLastSlice: UIView = self.info.copyOfLastSlice + { + self.slicesForLoop = [copyOfLastSlice] + .appending(contentsOf: self.info.slices) + .appending(copyOfFirstSlice) + } else { + self.slicesForLoop = self.info.slices + } + + super.init(frame: CGRect.zero) + setUpViewHierarchy() + } + + override init(frame: CGRect) { + preconditionFailure("Use init(attachment:) instead.") + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init(attachment:) instead.") + } + + private func setUpViewHierarchy() { + set(.width, to: self.info.sliceSize.width + Values.largeSpacing + 2 * self.info.arrowsSize.width) + set(.height, to: self.info.sliceSize.height) + + let stackView: UIStackView = UIStackView(arrangedSubviews: self.slicesForLoop) + stackView.axis = .horizontal + stackView.set(.width, to: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count)) + stackView.set(.height, to: self.info.sliceSize.height) + + addSubview(self.scrollView) + scrollView.center(in: self) + scrollView.set(.width, to: self.info.sliceSize.width) + scrollView.set(.height, to: self.info.sliceSize.height) + scrollView.addSubview(stackView) + scrollView.setContentOffset( + CGPoint( + x: Int(self.info.sliceSize.width) * (self.info.sliceCount > 1 ? 1 : 0), + y: 0 + ), + animated: false + ) + + addSubview(self.pageControl) + self.pageControl.center(.horizontal, in: self) + self.pageControl.pin(.bottom, to: .bottom, of: self) + + addSubview(self.arrowLeft) + self.arrowLeft.pin(.leading, to: .leading, of: self) + self.arrowLeft.center(.vertical, in: self) + + addSubview(self.arrowRight) + self.arrowRight.pin(.trailing, to: .trailing, of: self) + self.arrowRight.center(.vertical, in: self) + } + + // MARK: - UIScrollViewDelegate + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let pageIndex: Int = { + let maybeCurrentPageIndex: Int = Int(round(scrollView.contentOffset.x/self.info.sliceSize.width)) + if self.info.sliceCount > 1 { + if maybeCurrentPageIndex == 0 { + return pageControl.numberOfPages - 1 + } + if maybeCurrentPageIndex == self.slicesForLoop.count - 1 { + return 0 + } + return maybeCurrentPageIndex - 1 + } + return maybeCurrentPageIndex + }() + + pageControl.currentPage = pageIndex + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + setCorrectCotentOffsetIfNeeded(scrollView) + delegate?.carouselViewDidScrollToNewSlice(currentPage: pageControl.currentPage) + } + + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + setCorrectCotentOffsetIfNeeded(scrollView) + delegate?.carouselViewDidScrollToNewSlice(currentPage: pageControl.currentPage) + } + + private func setCorrectCotentOffsetIfNeeded(_ scrollView: UIScrollView) { + if pageControl.currentPage == 0 { + scrollView.setContentOffset( + CGPoint( + x: Int(self.info.sliceSize.width) * 1, + y: 0 + ), + animated: false + ) + } + + if pageControl.currentPage == pageControl.numberOfPages - 1 { + let realLastIndex: Int = self.slicesForLoop.count - 2 + scrollView.setContentOffset( + CGPoint( + x: Int(self.info.sliceSize.width) * realLastIndex, + y: 0 + ), + animated: false + ) + } + } + + // MARK: - Interaction + + @objc func scrollToNextSlice() { + self.scrollView.setContentOffset( + CGPoint( + x: self.scrollView.contentOffset.x + self.info.sliceSize.width, + y: 0 + ), + animated: true + ) + } + + @objc func scrollToPreviousSlice() { + self.scrollView.setContentOffset( + CGPoint( + x: self.scrollView.contentOffset.x - self.info.sliceSize.width, + y: 0 + ), + animated: true + ) + } +} diff --git a/Session/Shared/SessionCarouselViewDelegate.swift b/Session/Shared/SessionCarouselViewDelegate.swift new file mode 100644 index 000000000..8cb2e19eb --- /dev/null +++ b/Session/Shared/SessionCarouselViewDelegate.swift @@ -0,0 +1,7 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol SessionCarouselViewDelegate: AnyObject { + func carouselViewDidScrollToNewSlice(currentPage: Int) +} diff --git a/Session/Utilities/Date+Utilities.swift b/Session/Utilities/Date+Utilities.swift index 5336c20b4..ed7aab4f4 100644 --- a/Session/Utilities/Date+Utilities.swift +++ b/Session/Utilities/Date+Utilities.swift @@ -29,6 +29,14 @@ public extension Date { return "DATE_NOW".localized() } + + var fromattedForMessageInfo: String { + let formatter: DateFormatter = DateFormatter() + formatter.locale = Locale.current + formatter.dateFormat = "h:mm a EEE, DD/MM/YYYY" + + return formatter.string(from: self) + } } // MARK: - Formatters diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index c7261aafa..18aa47248 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -74,6 +74,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, public let id: Int64 public let variant: Interaction.Variant public let timestampMs: Int64 + public let receivedAtTimestampMs: Int64 public let authorId: String private let authorNameInternal: String? public let body: String? @@ -123,6 +124,9 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, /// This value will be used to populate the Context Menu and date header (if present) public var dateForUI: Date { Date(timeIntervalSince1970: (TimeInterval(self.timestampMs) / 1000)) } + /// This value will be used to populate the Message Info (if present) + public var receivedDateForUI: Date { Date(timeIntervalSince1970: (TimeInterval(self.receivedAtTimestampMs) / 1000)) } + /// This value specifies whether the body contains only emoji characters public let containsOnlyEmoji: Bool? @@ -164,6 +168,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, id: self.id, variant: self.variant, timestampMs: self.timestampMs, + receivedAtTimestampMs: self.receivedAtTimestampMs, authorId: self.authorId, authorNameInternal: self.authorNameInternal, body: self.body, @@ -321,6 +326,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, id: self.id, variant: self.variant, timestampMs: self.timestampMs, + receivedAtTimestampMs: self.receivedAtTimestampMs, authorId: self.authorId, authorNameInternal: self.authorNameInternal, body: (!self.variant.isInfoMessage ? @@ -500,6 +506,7 @@ public extension MessageViewModel { init( variant: Interaction.Variant = .standardOutgoing, timestampMs: Int64 = Int64.max, + receivedAtTimestampMs: Int64 = Int64.max, body: String? = nil, quote: Quote? = nil, cellType: CellType = .typingIndicator, @@ -527,6 +534,7 @@ public extension MessageViewModel { self.id = targetId self.variant = variant self.timestampMs = timestampMs + self.receivedAtTimestampMs = receivedAtTimestampMs self.authorId = "" self.authorNameInternal = nil self.body = body @@ -665,7 +673,7 @@ public extension MessageViewModel { let interactionAttachmentAttachmentIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) let interactionAttachmentAlbumIndexColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) - let numColumnsBeforeLinkedRecords: Int = 20 + let numColumnsBeforeLinkedRecords: Int = 21 let finalGroupSQL: SQL = (groupSQL ?? "") let request: SQLRequest = """ SELECT @@ -683,6 +691,7 @@ public extension MessageViewModel { \(interaction[.id]), \(interaction[.variant]), \(interaction[.timestampMs]), + \(interaction[.receivedAtTimestampMs]), \(interaction[.authorId]), IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), \(interaction[.body]), diff --git a/SessionUtilitiesKit/General/String+Utilities.swift b/SessionUtilitiesKit/General/String+Utilities.swift index 39bbbb913..44074f0da 100644 --- a/SessionUtilitiesKit/General/String+Utilities.swift +++ b/SessionUtilitiesKit/General/String+Utilities.swift @@ -84,6 +84,15 @@ public extension String { let secondsPerWeek: TimeInterval = (secondsPerDay * 7) switch format { + case .videoDuration: + let seconds: Int = Int(duration.truncatingRemainder(dividingBy: 60)) + let minutes: Int = Int((duration / 60).truncatingRemainder(dividingBy: 60)) + let hours: Int = Int(duration / 3600) + + guard hours > 0 else { return String(format: "%02ld:%02ld", minutes, seconds) } + + return String(format: "%ld:%02ld:%02ld", hours, minutes, seconds) + case .hoursMinutesSeconds: let seconds: Int = Int(duration.truncatingRemainder(dividingBy: 60)) let minutes: Int = Int((duration / 60).truncatingRemainder(dividingBy: 60)) diff --git a/SessionUtilitiesKit/General/TimeInterval+Utilities.swift b/SessionUtilitiesKit/General/TimeInterval+Utilities.swift index 89b3cc596..c0c668374 100644 --- a/SessionUtilitiesKit/General/TimeInterval+Utilities.swift +++ b/SessionUtilitiesKit/General/TimeInterval+Utilities.swift @@ -7,6 +7,7 @@ public extension TimeInterval { case short case long case hoursMinutesSeconds + case videoDuration } func formatted(format: DurationFormat) -> String {