749 lines
33 KiB
Swift
749 lines
33 KiB
Swift
//
|
|
// AttachmentChatTableViewCell.swift
|
|
//
|
|
// Siskin IM
|
|
// Copyright (C) 2019 "Tigase, Inc." <office@tigase.com>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. Look for COPYING file in the top folder.
|
|
// If not, see https://www.gnu.org/licenses/.
|
|
//
|
|
|
|
import UIKit
|
|
import MobileCoreServices
|
|
import LinkPresentation
|
|
import TigaseSwift
|
|
import AVKit
|
|
|
|
class AttachmentChatTableViewCell: BaseChatTableViewCell, UIContextMenuInteractionDelegate {
|
|
|
|
@IBOutlet var customView: UIView!;
|
|
|
|
override var backgroundColor: UIColor? {
|
|
didSet {
|
|
customView?.backgroundColor = backgroundColor;
|
|
}
|
|
}
|
|
|
|
fileprivate var tapGestureRecognizer: UITapGestureRecognizer?;
|
|
fileprivate var longPressGestureRecognizer: UILongPressGestureRecognizer?;
|
|
|
|
private var item: ChatAttachment?;
|
|
|
|
private var linkView: UIView? {
|
|
didSet {
|
|
// if let old = oldValue, let new = linkView {
|
|
// guard old != new else {
|
|
// return;
|
|
// }
|
|
// }
|
|
if let view = oldValue {
|
|
view.removeFromSuperview();
|
|
}
|
|
if let view = linkView {
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
self.customView.addSubview(view);
|
|
if #available(iOS 13.0, *) {
|
|
view.addInteraction(UIContextMenuInteraction(delegate: self));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func awakeFromNib() {
|
|
super.awakeFromNib();
|
|
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapGestureDidFire));
|
|
tapGestureRecognizer?.cancelsTouchesInView = false;
|
|
tapGestureRecognizer?.numberOfTapsRequired = 1;
|
|
customView.addGestureRecognizer(tapGestureRecognizer!);
|
|
|
|
if #available(iOS 13.0, *) {
|
|
customView.addInteraction(UIContextMenuInteraction(delegate: self));
|
|
} else {
|
|
longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressGestureDidFire));
|
|
longPressGestureRecognizer?.cancelsTouchesInView = true;
|
|
longPressGestureRecognizer?.delegate = self;
|
|
|
|
customView.addGestureRecognizer(longPressGestureRecognizer!);
|
|
}
|
|
}
|
|
|
|
lazy var playButton: UIButton = {
|
|
let playButton = UIButton()
|
|
playButton.setImage(UIImage(named: "play.fill"), for: .normal)
|
|
playButton.setImage(UIImage(named: "pause.fill"), for: .selected)
|
|
playButton.addTarget(self, action: #selector(playPauseTapped), for: .touchUpInside)
|
|
playButton.translatesAutoresizingMaskIntoConstraints = false
|
|
return playButton
|
|
}()
|
|
lazy var slider: CustomSlider = {
|
|
let slider = CustomSlider()
|
|
slider.setThumbRadius(radius: 15)
|
|
slider.addTarget(self, action: #selector(sliderScrubber(sender:)), for: .valueChanged)
|
|
slider.translatesAutoresizingMaskIntoConstraints = false
|
|
return slider
|
|
}()
|
|
lazy var audioTime: UILabel = {
|
|
let audioTime = UILabel()
|
|
audioTime.text = "00:00"
|
|
audioTime.textColor = UIColor(named: "tintColor")
|
|
audioTime.font = UIFont.systemFont(ofSize: 12)
|
|
audioTime.setContentHuggingPriority(.required, for: .horizontal)
|
|
audioTime.translatesAutoresizingMaskIntoConstraints = false
|
|
return audioTime
|
|
}()
|
|
|
|
var audioPlayer: AVAudioPlayer?
|
|
var audioTimer: Foundation.Timer?
|
|
var sliderTimer: Foundation.Timer?
|
|
|
|
func setupAudioCell(item: ChatAttachment) {
|
|
self.customView.backgroundColor = .clear
|
|
self.customView.subviews.forEach { $0.removeFromSuperview() }
|
|
if let gesture = tapGestureRecognizer {
|
|
self.customView.removeGestureRecognizer(gesture)
|
|
}
|
|
|
|
setupAudioPlayer()
|
|
|
|
self.customView.addSubview(playButton)
|
|
self.customView.addSubview(slider)
|
|
self.customView.addSubview(audioTime)
|
|
|
|
let sliderWidth = UIScreen.main.bounds.width * 0.30
|
|
|
|
NSLayoutConstraint.activate([
|
|
playButton.topAnchor.constraint(equalTo: customView.topAnchor),
|
|
playButton.bottomAnchor.constraint(equalTo: customView.bottomAnchor),
|
|
playButton.leadingAnchor.constraint(equalTo: customView.leadingAnchor),
|
|
playButton.widthAnchor.constraint(equalToConstant: 17),
|
|
playButton.heightAnchor.constraint(equalToConstant: 17),
|
|
|
|
slider.centerYAnchor.constraint(equalTo: playButton.centerYAnchor),
|
|
slider.leadingAnchor.constraint(equalTo: playButton.trailingAnchor, constant: 10),
|
|
slider.widthAnchor.constraint(equalToConstant: sliderWidth),
|
|
|
|
audioTime.centerYAnchor.constraint(equalTo: slider.centerYAnchor),
|
|
audioTime.leadingAnchor.constraint(equalTo: slider.trailingAnchor, constant: 10),
|
|
audioTime.trailingAnchor.constraint(equalTo: customView.trailingAnchor, constant: -5)
|
|
])
|
|
}
|
|
|
|
func setupAudioPlayer() {
|
|
guard let item = self.item else { return }
|
|
|
|
let session = AVAudioSession.sharedInstance()
|
|
|
|
do {
|
|
try session.setCategory(.playback, mode: .default)
|
|
try session.setActive(true)
|
|
} catch {
|
|
|
|
}
|
|
|
|
if let localUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
|
do {
|
|
audioPlayer = try AVAudioPlayer(contentsOf: localUrl)
|
|
audioPlayer?.prepareToPlay()
|
|
audioPlayer?.volume = 1.0
|
|
self.audioTime.text = audioPlayer?.duration.stringTime
|
|
|
|
}
|
|
catch let error as NSError {
|
|
print(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func playPauseTapped() {
|
|
let isSelected = self.playButton.isSelected
|
|
self.playButton.isSelected = !isSelected
|
|
|
|
if !isSelected {
|
|
|
|
audioTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateAudioTime), userInfo: nil, repeats: true)
|
|
sliderTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateSlider), userInfo: nil, repeats: true)
|
|
|
|
audioPlayer?.play()
|
|
|
|
if let player = self.audioPlayer, let audioTimer = self.audioTimer, let sliderTimer = self.sliderTimer {
|
|
self.cellDelegate?.didPlayAudio(audioPlayer: player, audioTimer: audioTimer, sliderTimer: sliderTimer, playButton: self.playButton)
|
|
}
|
|
|
|
}
|
|
else {
|
|
audioPlayer?.pause()
|
|
self.cellDelegate?.didStopAudio()
|
|
}
|
|
}
|
|
|
|
@objc func sliderScrubber(sender: UISlider) {
|
|
guard let player = self.audioPlayer else { return }
|
|
|
|
let value = Double(sender.value) * player.duration
|
|
player.currentTime = value
|
|
}
|
|
|
|
@objc func updateSlider() {
|
|
guard let player = self.audioPlayer else {
|
|
sliderTimer?.invalidate()
|
|
return
|
|
}
|
|
if !player.isPlaying {
|
|
sliderTimer?.invalidate()
|
|
}
|
|
|
|
slider.value = Float(player.currentTime/player.duration)
|
|
|
|
if slider.value == 0.0, !player.isPlaying {
|
|
self.playButton.isSelected = false
|
|
self.cellDelegate?.didStopAudio()
|
|
}
|
|
}
|
|
|
|
@objc func updateAudioTime() {
|
|
guard let player = audioPlayer else {
|
|
audioTimer?.invalidate()
|
|
return
|
|
}
|
|
if !player.isPlaying {
|
|
audioTimer?.invalidate()
|
|
return
|
|
}
|
|
|
|
let currentTime = Int(player.currentTime)
|
|
|
|
let minutes = currentTime/60
|
|
let seconds = currentTime - minutes / 60
|
|
|
|
audioTime.text = NSString(format: "%02d:%02d", minutes,seconds) as String
|
|
}
|
|
|
|
func set(attachment item: ChatAttachment, maxImageWidth: CGFloat, indexPath: IndexPath) {
|
|
self.item = item;
|
|
self.customView.subviews.forEach { view in
|
|
view.removeFromSuperview()
|
|
}
|
|
|
|
super.set(item: item, indexPath: indexPath)
|
|
|
|
self.customView?.isOpaque = true;
|
|
self.customView?.backgroundColor = self.backgroundColor;
|
|
|
|
if let mime = item.appendix.mimetype, mime.contains("audio") {
|
|
setupAudioCell(item: item)
|
|
return
|
|
}
|
|
|
|
if let localUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
|
documentController = UIDocumentInteractionController(url: localUrl);
|
|
let attachmentInfo = (self.linkView as? AttachmentInfoView) ?? AttachmentInfoView(frame: .zero);
|
|
attachmentInfo.translatesAutoresizingMaskIntoConstraints = false
|
|
self.linkView = attachmentInfo;
|
|
NSLayoutConstraint.activate([
|
|
attachmentInfo.leadingAnchor.constraint(equalTo: customView.leadingAnchor),
|
|
attachmentInfo.trailingAnchor.constraint(equalTo: customView.trailingAnchor),
|
|
attachmentInfo.topAnchor.constraint(equalTo: customView.topAnchor),
|
|
attachmentInfo.bottomAnchor.constraint(equalTo: customView.bottomAnchor)
|
|
])
|
|
attachmentInfo.set(item: item, maxImageWidth: maxImageWidth)
|
|
let fileSize = MediaHelper.fileSizeToString(try! FileManager.default.attributesOfItem(atPath: localUrl.path)[.size] as? UInt64)
|
|
let time = timestampView?.text ?? ""
|
|
timestampView?.text = item.state.direction == .incoming ? "\(time) · \(fileSize)" : "\(fileSize) · \(time)"
|
|
} else {
|
|
documentController = nil;
|
|
|
|
let attachmentInfo = (self.linkView as? AttachmentInfoView) ?? AttachmentInfoView(frame: .zero);
|
|
self.linkView = attachmentInfo;
|
|
NSLayoutConstraint.activate([
|
|
customView.leadingAnchor.constraint(equalTo: attachmentInfo.leadingAnchor),
|
|
customView.trailingAnchor.constraint(equalTo: attachmentInfo.trailingAnchor),
|
|
customView.topAnchor.constraint(equalTo: attachmentInfo.topAnchor),
|
|
customView.bottomAnchor.constraint(equalTo: attachmentInfo.bottomAnchor)
|
|
])
|
|
attachmentInfo.set(item: item, maxImageWidth: maxImageWidth)
|
|
|
|
switch item.appendix.state {
|
|
case .new:
|
|
let sizeLimit = Settings.fileDownloadSizeLimit.integer();
|
|
if sizeLimit > 0 {
|
|
if let sessionObject = XmppService.instance.getClient(for: item.account)?.sessionObject, (RosterModule.getRosterStore(sessionObject).get(for: JID(item.jid))?.subscription ?? .none).isFrom || (DBChatStore.instance.getChat(for: item.account, with: item.jid) as? Room != nil) {
|
|
_ = DownloadManager.instance.download(item: item, maxSize: sizeLimit >= Int.max ? Int64.max : Int64(sizeLimit * 1024 * 1024));
|
|
attachmentInfo.progress(show: true);
|
|
return;
|
|
}
|
|
}
|
|
attachmentInfo.progress(show: DownloadManager.instance.downloadInProgress(for: item));
|
|
default:
|
|
attachmentInfo.progress(show: DownloadManager.instance.downloadInProgress(for: item));
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 13.0, *)
|
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions -> UIMenu? in
|
|
return self.prepareContextMenu();
|
|
};
|
|
}
|
|
|
|
@available(iOS 13.0, *)
|
|
func prepareContextMenu() -> UIMenu {
|
|
guard let item = self.item else {
|
|
return UIMenu(title: "");
|
|
}
|
|
|
|
if let localUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
|
let items = [
|
|
UIAction(title: NSLocalizedString("Preview", comment: "Alert title"), image: UIImage(systemName: "eye.fill"), handler: { action in
|
|
print("preview called");
|
|
self.open(url: localUrl, preview: true);
|
|
}),
|
|
UIAction(title: NSLocalizedString("Copy", comment: "Context menu action"), image: UIImage(systemName: "doc.on.doc"), handler: { action in
|
|
guard let text = self.item?.copyText(withTimestamp: Settings.CopyMessagesWithTimestamps.getBool(), withSender: false) else {
|
|
return;
|
|
}
|
|
UIPasteboard.general.strings = [text];
|
|
UIPasteboard.general.string = text;
|
|
}),
|
|
UIAction(title: NSLocalizedString("Share...", comment: "Context menu action"), image: UIImage(systemName: "square.and.arrow.up"), handler: { action in
|
|
print("share called");
|
|
self.open(url: localUrl, preview: false);
|
|
}),
|
|
UIAction(title: NSLocalizedString("Delete", comment: "Context menu action"), image: UIImage(systemName: "trash"), attributes: [.destructive], handler: { action in
|
|
print("delete called");
|
|
DownloadStore.instance.deleteFile(for: "\(item.id)");
|
|
DBChatHistoryStore.instance.updateItem(for: item.account, with: item.jid, id: item.id, updateAppendix: { appendix in
|
|
appendix.state = .removed;
|
|
})
|
|
}),
|
|
UIAction(title: NSLocalizedString("More...", comment: "Menu item: view more options"), image: UIImage(systemName: "ellipsis"), handler: { action in
|
|
NotificationCenter.default.post(name: Notification.Name("tableViewCellShowEditToolbar"), object: self);
|
|
})
|
|
];
|
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: items);
|
|
} else {
|
|
let items = [
|
|
UIAction(title: NSLocalizedString("Copy", comment: "Context menu item"), image: UIImage(systemName: "doc.on.doc"), handler: { action in
|
|
guard let text = self.item?.copyText(withTimestamp: Settings.CopyMessagesWithTimestamps.getBool(), withSender: false) else {
|
|
return;
|
|
}
|
|
UIPasteboard.general.strings = [text];
|
|
UIPasteboard.general.string = text;
|
|
}),
|
|
UIAction(title: NSLocalizedString("Download", comment: "Context menu item"), image: UIImage(systemName: "square.and.arrow.down"), handler: { action in
|
|
print("download called");
|
|
self.download(for: item);
|
|
}),
|
|
UIAction(title: NSLocalizedString("More...", comment: "Context menu item: view more options"), image: UIImage(systemName: "ellipsis"), handler: { action in
|
|
NotificationCenter.default.post(name: Notification.Name("tableViewCellShowEditToolbar"), object: self);
|
|
})
|
|
];
|
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: items);
|
|
}
|
|
}
|
|
|
|
@objc func longPressGestureDidFire(_ recognizer: UILongPressGestureRecognizer) {
|
|
guard recognizer.state == .recognized else {
|
|
return;
|
|
}
|
|
downloadOrOpen();
|
|
}
|
|
|
|
@objc func tapGestureDidFire(_ recognizer: UITapGestureRecognizer) {
|
|
downloadOrOpen();
|
|
}
|
|
|
|
var documentController: UIDocumentInteractionController? {
|
|
didSet {
|
|
if let value = oldValue {
|
|
for recognizer in value.gestureRecognizers {
|
|
self.removeGestureRecognizer(recognizer)
|
|
}
|
|
}
|
|
if let value = documentController {
|
|
value.delegate = self;
|
|
for recognizer in value.gestureRecognizers {
|
|
self.addGestureRecognizer(recognizer)
|
|
}
|
|
}
|
|
longPressGestureRecognizer?.isEnabled = documentController == nil;
|
|
}
|
|
}
|
|
|
|
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
|
|
let rootViewController = ((UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController)!;
|
|
if let viewController = rootViewController.presentingViewController {
|
|
return viewController;
|
|
}
|
|
return rootViewController;
|
|
}
|
|
|
|
func open(url: URL, preview: Bool) {
|
|
print("opening a file:", url, "exists:", FileManager.default.fileExists(atPath: url.path));// "tmp:", tmpUrl);
|
|
let documentController = UIDocumentInteractionController(url: url);
|
|
documentController.delegate = self;
|
|
print("detected uti:", documentController.uti as Any, "for:", documentController.url as Any);
|
|
if preview && documentController.presentPreview(animated: true) {
|
|
self.documentController = documentController;
|
|
} else if documentController.presentOptionsMenu(from: self.superview?.convert(self.frame, to: self.superview?.superview) ?? CGRect.zero, in: self.self, animated: true) {
|
|
self.documentController = documentController;
|
|
}
|
|
}
|
|
|
|
func download(for item: ChatAttachment) {
|
|
_ = DownloadManager.instance.download(item: item, maxSize: Int64.max);
|
|
(self.linkView as? AttachmentInfoView)?.progress(show: true);
|
|
}
|
|
|
|
private func downloadOrOpen() {
|
|
guard let item = self.item else {
|
|
return;
|
|
}
|
|
if let localUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
|
open(url: localUrl, preview: true);
|
|
} else {
|
|
let alert = UIAlertController(title: NSLocalizedString("Download", comment: "Alert title"), message: NSLocalizedString("File is not available locally. Should it be downloaded?", comment: "Alert text"), preferredStyle: .alert);
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("Yes", comment: ""), style: .default, handler: { (action) in
|
|
self.download(for: item);
|
|
}))
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("No", comment: ""), style: .cancel, handler: nil));
|
|
if let controller = (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController {
|
|
controller.present(alert, animated: true, completion: nil);
|
|
}
|
|
}
|
|
}
|
|
|
|
class AttachmentInfoView: UIView {
|
|
|
|
var imageWidthConstraint: NSLayoutConstraint?
|
|
var imageHeightConstraint: NSLayoutConstraint?
|
|
|
|
let iconView: ImageAttachmentPreview;
|
|
let filename: UILabel;
|
|
let details: UILabel;
|
|
let playImage: UIImageView
|
|
|
|
private var viewType: ViewType = .none {
|
|
didSet {
|
|
guard viewType != oldValue else {
|
|
return;
|
|
}
|
|
switch oldValue {
|
|
case .none:
|
|
break;
|
|
case .file:
|
|
NSLayoutConstraint.deactivate(fileViewConstraints);
|
|
case .imagePreview, .videoPreview:
|
|
NSLayoutConstraint.deactivate(imagePreviewConstraints);
|
|
}
|
|
switch viewType {
|
|
case .none:
|
|
break;
|
|
case .file:
|
|
addSubview(filename)
|
|
addSubview(details)
|
|
self.imageWidthConstraint?.isActive = false
|
|
self.imageHeightConstraint?.isActive = false
|
|
NSLayoutConstraint.activate(fileViewConstraints);
|
|
case .imagePreview:
|
|
filename.removeFromSuperview()
|
|
details.removeFromSuperview()
|
|
playImage.removeFromSuperview()
|
|
NSLayoutConstraint.activate(imagePreviewConstraints)
|
|
case .videoPreview:
|
|
filename.removeFromSuperview()
|
|
details.removeFromSuperview()
|
|
NSLayoutConstraint.activate(imagePreviewConstraints)
|
|
addPlayImage()
|
|
}
|
|
iconView.contentMode = .scaleAspectFit
|
|
iconView.isImagePreview = true
|
|
}
|
|
}
|
|
|
|
private var fileViewConstraints: [NSLayoutConstraint] = [];
|
|
private var imagePreviewConstraints: [NSLayoutConstraint] = [];
|
|
|
|
override init(frame: CGRect) {
|
|
iconView = ImageAttachmentPreview(frame: .zero)
|
|
iconView.clipsToBounds = true
|
|
iconView.translatesAutoresizingMaskIntoConstraints = false;
|
|
|
|
filename = UILabel(frame: .zero);
|
|
filename.font = UIFont.systemFont(ofSize: UIFont.systemFontSize - 1, weight: .semibold);
|
|
filename.translatesAutoresizingMaskIntoConstraints = false;
|
|
filename.setContentHuggingPriority(.defaultHigh, for: .horizontal);
|
|
filename.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal);
|
|
|
|
details = UILabel(frame: .zero);
|
|
details.font = UIFont.systemFont(ofSize: UIFont.systemFontSize - 2, weight: .regular);
|
|
details.translatesAutoresizingMaskIntoConstraints = false;
|
|
details.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
details.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal);
|
|
|
|
playImage = UIImageView()
|
|
playImage.image = UIImage(named: "play.fill")
|
|
playImage.translatesAutoresizingMaskIntoConstraints = false
|
|
playImage.widthAnchor.constraint(equalToConstant: 30).isActive = true
|
|
playImage.heightAnchor.constraint(equalToConstant: 30).isActive = true
|
|
|
|
super.init(frame: frame);
|
|
self.clipsToBounds = true
|
|
self.translatesAutoresizingMaskIntoConstraints = false;
|
|
self.isOpaque = false;
|
|
|
|
addSubview(iconView);
|
|
|
|
fileViewConstraints = [
|
|
iconView.heightAnchor.constraint(equalToConstant: 30),
|
|
iconView.widthAnchor.constraint(equalTo: iconView.heightAnchor),
|
|
|
|
iconView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 12),
|
|
iconView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
|
|
iconView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8),
|
|
|
|
filename.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12),
|
|
filename.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
|
|
filename.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -12),
|
|
|
|
details.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 12),
|
|
details.topAnchor.constraint(equalTo: filename.bottomAnchor, constant: 0),
|
|
details.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8),
|
|
// -- this is causing issue with progress indicatior!!
|
|
details.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -12),
|
|
details.heightAnchor.constraint(equalTo: filename.heightAnchor)
|
|
];
|
|
|
|
imagePreviewConstraints = [
|
|
iconView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
|
iconView.topAnchor.constraint(equalTo: self.topAnchor),
|
|
iconView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
|
iconView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
|
|
|
];
|
|
self.imageWidthConstraint = iconView.widthAnchor.constraint(equalToConstant: 0)
|
|
self.imageHeightConstraint = iconView.heightAnchor.constraint(equalToConstant: 0)
|
|
}
|
|
|
|
func addPlayImage() {
|
|
self.addSubview(playImage)
|
|
playImage.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
|
|
playImage.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
|
|
}
|
|
|
|
func setImageConstraints(image: UIImage?, maxImageWidth: CGFloat) {
|
|
guard let image = image else { return }
|
|
if viewType == .file { return }
|
|
|
|
var scale: CGFloat = 0.0
|
|
var height = UIScreen.main.bounds.height
|
|
height *= 0.4
|
|
if image.size.width > image.size.height {
|
|
scale = maxImageWidth / image.size.width
|
|
} else {
|
|
scale = height / image.size.height
|
|
}
|
|
|
|
self.imageWidthConstraint?.constant = image.size.width * scale
|
|
self.imageHeightConstraint?.constant = image.size.height * scale
|
|
self.imageWidthConstraint?.isActive = true
|
|
self.imageHeightConstraint?.isActive = true
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
return nil;
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
let path = UIBezierPath(roundedRect: rect, cornerRadius: 5);
|
|
path.addClip();
|
|
if #available(iOS 13.0, *) {
|
|
UIColor.secondarySystemBackground.setFill();
|
|
} else {
|
|
UIColor.lightGray.withAlphaComponent(0.5).setFill();
|
|
}
|
|
path.fill();
|
|
|
|
super.draw(rect);
|
|
}
|
|
|
|
func set(item: ChatAttachment, maxImageWidth: CGFloat) {
|
|
if let fileUrl = DownloadStore.instance.url(for: "\(item.id)") {
|
|
filename.text = fileUrl.lastPathComponent;
|
|
let fileSize = MediaHelper.fileSizeToString(try! FileManager.default.attributesOfItem(atPath: fileUrl.path)[.size] as? UInt64);
|
|
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileUrl.pathExtension as CFString, nil)?.takeRetainedValue(), let typeName = UTTypeCopyDescription(uti)?.takeRetainedValue() as String? {
|
|
details.text = "\(typeName) - \(fileSize)";
|
|
|
|
if UTTypeConformsTo(uti, kUTTypeImage) {
|
|
iconView.image = UIImage(contentsOfFile: fileUrl.path)
|
|
self.viewType = .imagePreview;
|
|
self.setImageConstraints(image: iconView.image, maxImageWidth: maxImageWidth)
|
|
} else if UTTypeConformsTo(uti, kUTTypeMovie) {
|
|
iconView.image = MediaHelper.generateThumbnail(url: fileUrl)
|
|
self.viewType = .videoPreview
|
|
self.setImageConstraints(image: iconView.image, maxImageWidth: maxImageWidth)
|
|
} else {
|
|
self.viewType = .file;
|
|
iconView.image = UIImage.icon(forFile: fileUrl, mimeType: item.appendix.mimetype);
|
|
}
|
|
} else {
|
|
details.text = fileSize;
|
|
iconView.image = UIImage.icon(forFile: fileUrl, mimeType: item.appendix.mimetype);
|
|
self.viewType = .file;
|
|
}
|
|
} else {
|
|
let filename = item.appendix.filename ?? URL(string: item.url)?.lastPathComponent ?? "";
|
|
if filename.isEmpty {
|
|
self.filename.text = "Unknown file";
|
|
} else {
|
|
self.filename.text = filename;
|
|
}
|
|
if let size = item.appendix.filesize {
|
|
if let mimetype = item.appendix.mimetype, let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimetype as CFString, nil)?.takeRetainedValue(), let typeName = UTTypeCopyDescription(uti)?.takeRetainedValue() as String? {
|
|
let fileSize = size >= 0 ? MediaHelper.fileSizeToString(UInt64(size)) : "";
|
|
details.text = "\(typeName) - \(fileSize)";
|
|
iconView.image = UIImage.icon(forUTI: uti as String);
|
|
} else {
|
|
details.text = MediaHelper.fileSizeToString(UInt64(size));
|
|
iconView.image = UIImage.icon(forUTI: "public.content");
|
|
}
|
|
} else {
|
|
details.text = "--";
|
|
iconView.image = UIImage.icon(forUTI: "public.content");
|
|
}
|
|
self.viewType = .file;
|
|
}
|
|
}
|
|
|
|
var progressView: UIActivityIndicatorView?;
|
|
|
|
func progress(show: Bool) {
|
|
guard show != (progressView != nil) else {
|
|
return;
|
|
}
|
|
|
|
if show {
|
|
let view = UIActivityIndicatorView(style: .gray);
|
|
view.translatesAutoresizingMaskIntoConstraints = false;
|
|
self.addSubview(view);
|
|
NSLayoutConstraint.activate([
|
|
view.leadingAnchor.constraint(greaterThanOrEqualTo: filename.trailingAnchor, constant: 8),
|
|
view.leadingAnchor.constraint(greaterThanOrEqualTo: details.trailingAnchor, constant: 8),
|
|
view.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12),
|
|
view.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
|
view.topAnchor.constraint(lessThanOrEqualTo: self.topAnchor)
|
|
])
|
|
self.progressView = view;
|
|
view.startAnimating();
|
|
} else if let view = progressView {
|
|
view.stopAnimating();
|
|
self.progressView = nil;
|
|
view.removeFromSuperview();
|
|
}
|
|
}
|
|
|
|
enum ViewType {
|
|
case none
|
|
case file
|
|
case imagePreview
|
|
case videoPreview
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
class ImageAttachmentPreview: UIImageView {
|
|
|
|
var isImagePreview: Bool = false {
|
|
didSet {
|
|
if isImagePreview != oldValue {
|
|
if isImagePreview {
|
|
self.layer.cornerRadius = 5
|
|
//self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner];
|
|
} else {
|
|
self.layer.cornerRadius = 0;
|
|
self.layer.maskedCorners = [];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var contentClippingRect: CGRect {
|
|
guard let image = image else { return bounds }
|
|
guard contentMode == .scaleAspectFit else { return bounds }
|
|
guard image.size.width > 0 && image.size.height > 0 else { return bounds }
|
|
|
|
let scale: CGFloat
|
|
if image.size.width > image.size.height {
|
|
scale = bounds.width / image.size.width
|
|
} else {
|
|
scale = bounds.height / image.size.height
|
|
}
|
|
|
|
let size = CGSize(width: image.size.width * scale, height: image.size.height * scale)
|
|
let x = (bounds.width - size.width) / 2.0
|
|
let y = (bounds.height - size.height) / 2.0
|
|
|
|
return CGRect(x: x, y: y, width: size.width, height: size.height)
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame);
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
}
|
|
|
|
extension FileManager {
|
|
public func fileExtension(forUTI utiString: String) -> String? {
|
|
guard
|
|
let cfFileExtension = UTTypeCopyPreferredTagWithClass(utiString as CFString, kUTTagClassFilenameExtension)?.takeRetainedValue() else
|
|
{
|
|
return nil
|
|
}
|
|
|
|
return cfFileExtension as String
|
|
}
|
|
}
|
|
|
|
extension UIImage {
|
|
class func icon(forFile url: URL, mimeType: String?) -> UIImage? {
|
|
let controller = UIDocumentInteractionController(url: url);
|
|
if mimeType != nil, let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType! as CFString, nil)?.takeRetainedValue() as String? {
|
|
controller.uti = uti;
|
|
}
|
|
if controller.icons.count == 0 {
|
|
controller.uti = "public.data";
|
|
}
|
|
let icons = controller.icons;
|
|
print("got:", icons.last as Any, "for:", url.absoluteString);
|
|
return icons.last;
|
|
}
|
|
|
|
class func icon(forUTI utiString: String) -> UIImage? {
|
|
let controller = UIDocumentInteractionController(url: URL(fileURLWithPath: "temp.file"));
|
|
controller.uti = utiString;
|
|
if controller.icons.count == 0 {
|
|
controller.uti = "public.data";
|
|
}
|
|
let icons = controller.icons;
|
|
print("got:", icons.last as Any, "for:", utiString);
|
|
return icons.last;
|
|
}
|
|
|
|
}
|