Merge pull request #782 from RyanRory/message-and-image-info

Media info
This commit is contained in:
Morgan Pretty 2023-04-13 12:07:22 +10:00 committed by GitHub
commit 7e39ed369f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 990 additions and 13 deletions

View file

@ -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 = "<group>"; };
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = "<group>"; };
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaInfoView.swift"; sourceTree = "<group>"; };
7B2561C329874851005C086C /* SessionCarouselView+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCarouselView+Info.swift"; sourceTree = "<group>"; };
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInfoVC.swift; sourceTree = "<group>"; };
7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaPreviewView.swift"; sourceTree = "<group>"; };
7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselView.swift; sourceTree = "<group>"; };
7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselViewDelegate.swift; sourceTree = "<group>"; };
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = "<group>"; };
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = "<group>"; };
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 = "<group>";
@ -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 */,

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -29,7 +29,7 @@ public class MediaAlbumView: UIStackView {
mediaCache: mediaCache,
attachment: $0,
isOutgoing: isOutgoing,
maxMessageWidth: maxMessageWidth
cornerRadius: VisibleMessageCell.largeCornerRadius
)
}

View file

@ -16,10 +16,9 @@ public class MediaView: UIView {
// MARK: -
private let mediaCache: NSCache<NSString, AnyObject>
private let mediaCache: NSCache<NSString, AnyObject>?
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<NSString, AnyObject>,
mediaCache: NSCache<NSString, AnyObject>? = 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 {

View file

@ -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)
}()
}
}
}

View file

@ -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)
}
}
}

View file

@ -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])
}
}

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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
}
}
}

View file

@ -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
)
}
}

View file

@ -0,0 +1,7 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public protocol SessionCarouselViewDelegate: AnyObject {
func carouselViewDidScrollToNewSlice(currentPage: Int)
}

View file

@ -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

View file

@ -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<ViewModel> = """
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]),

View file

@ -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))

View file

@ -7,6 +7,7 @@ public extension TimeInterval {
case short
case long
case hoursMinutesSeconds
case videoDuration
}
func formatted(format: DurationFormat) -> String {