mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge pull request #782 from RyanRory/message-and-image-info
Media info
This commit is contained in:
commit
7e39ed369f
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ public class MediaAlbumView: UIStackView {
|
|||
mediaCache: mediaCache,
|
||||
attachment: $0,
|
||||
isOutgoing: isOutgoing,
|
||||
maxMessageWidth: maxMessageWidth
|
||||
cornerRadius: VisibleMessageCell.largeCornerRadius
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
191
Session/Media Viewing & Editing/MediaInfoVC+MediaInfoView.swift
Normal file
191
Session/Media Viewing & Editing/MediaInfoVC+MediaInfoView.swift
Normal 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)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
149
Session/Media Viewing & Editing/MediaInfoVC.swift
Normal file
149
Session/Media Viewing & Editing/MediaInfoVC.swift
Normal 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])
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
72
Session/Shared/SessionCarouselView+Info.swift
Normal file
72
Session/Shared/SessionCarouselView+Info.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
202
Session/Shared/SessionCarouselView.swift
Normal file
202
Session/Shared/SessionCarouselView.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
7
Session/Shared/SessionCarouselViewDelegate.swift
Normal file
7
Session/Shared/SessionCarouselViewDelegate.swift
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol SessionCarouselViewDelegate: AnyObject {
|
||||
func carouselViewDidScrollToNewSlice(currentPage: Int)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -7,6 +7,7 @@ public extension TimeInterval {
|
|||
case short
|
||||
case long
|
||||
case hoursMinutesSeconds
|
||||
case videoDuration
|
||||
}
|
||||
|
||||
func formatted(format: DurationFormat) -> String {
|
||||
|
|
Loading…
Reference in a new issue