123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- //
- // SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- // SPDX-License-Identifier: GPL-3.0-or-later
- //
- extension BaseChatTableViewCell {
- func setupForFileCell(with message: NCChatMessage, with account: TalkAccount) {
- if self.filePreviewImageView == nil {
- // Preview image view
- let filePreviewImageView = FilePreviewImageView(frame: .init(x: 0, y: 0, width: fileMessageCellFileMaxPreviewHeight, height: fileMessageCellFileMaxPreviewWidth))
- self.filePreviewImageView = filePreviewImageView
- filePreviewImageView.translatesAutoresizingMaskIntoConstraints = false
- filePreviewImageView.layer.cornerRadius = chatMessageCellPreviewCornerRadius
- filePreviewImageView.layer.masksToBounds = true
- filePreviewImageView.contentMode = .scaleAspectFit
- self.messageBodyView.addSubview(filePreviewImageView)
- let previewTap = UITapGestureRecognizer(target: self, action: #selector(filePreviewTapped))
- filePreviewImageView.addGestureRecognizer(previewTap)
- filePreviewImageView.isUserInteractionEnabled = true
- // PlayIcon for video files with preview
- let filePreviewPlayIconImageView = UIImageView(frame: .init(x: 0, y: 0, width: fileMessageCellFileMaxPreviewHeight, height: fileMessageCellFileMaxPreviewWidth))
- self.filePreviewPlayIconImageView = filePreviewPlayIconImageView
- filePreviewPlayIconImageView.isHidden = true
- filePreviewPlayIconImageView.tintColor = .init(white: 1.0, alpha: 0.8)
- filePreviewPlayIconImageView.image = .init(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(weight: .black))
- filePreviewImageView.addSubview(filePreviewPlayIconImageView)
- filePreviewImageView.bringSubviewToFront(filePreviewPlayIconImageView)
- // Activity indicator while loading previews
- let filePreviewActivityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: fileMessageCellMinimumHeight, height: fileMessageCellMinimumHeight))
- self.filePreviewActivityIndicator = filePreviewActivityIndicator
- filePreviewActivityIndicator.translatesAutoresizingMaskIntoConstraints = false
- filePreviewActivityIndicator.radius = fileMessageCellMinimumHeight / 2
- filePreviewActivityIndicator.cycleColors = [.systemGray2]
- filePreviewActivityIndicator.indicatorMode = .indeterminate
- filePreviewImageView.addSubview(filePreviewActivityIndicator)
- NSLayoutConstraint.activate([
- filePreviewActivityIndicator.centerYAnchor.constraint(equalTo: filePreviewImageView.centerYAnchor),
- filePreviewActivityIndicator.centerXAnchor.constraint(equalTo: filePreviewImageView.centerXAnchor)
- ])
- // Add everything to messageBodyView
- let heightConstraint = filePreviewImageView.heightAnchor.constraint(equalToConstant: fileMessageCellFileMaxPreviewHeight)
- let widthConstraint = filePreviewImageView.widthAnchor.constraint(equalToConstant: fileMessageCellFileMaxPreviewWidth)
- self.filePreviewImageViewHeightConstraint = heightConstraint
- self.filePreviewImageViewWidthConstraint = widthConstraint
- let messageTextView = MessageBodyTextView()
- self.messageTextView = messageTextView
- messageTextView.translatesAutoresizingMaskIntoConstraints = false
- self.messageBodyView.addSubview(messageTextView)
- NSLayoutConstraint.activate([
- filePreviewImageView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
- filePreviewImageView.topAnchor.constraint(equalTo: self.messageBodyView.topAnchor),
- heightConstraint,
- widthConstraint,
- messageTextView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
- messageTextView.rightAnchor.constraint(equalTo: self.messageBodyView.rightAnchor),
- messageTextView.topAnchor.constraint(equalTo: filePreviewImageView.bottomAnchor, constant: 10),
- messageTextView.bottomAnchor.constraint(equalTo: self.messageBodyView.bottomAnchor)
- ])
- }
- guard let filePreviewImageView = self.filePreviewImageView,
- let messageTextView = self.messageTextView
- else { return }
- messageTextView.attributedText = message.parsedMarkdownForChat()
- if message.message == "{file}" {
- messageTextView.dataDetectorTypes = []
- } else {
- messageTextView.dataDetectorTypes = .all
- }
- self.requestPreview(for: message, with: account)
- if !message.sendingFailed {
- if message.isTemporary {
- self.addActivityIndicator(with: 0)
- } else if let fileStatus = message.file().fileStatus {
- if fileStatus.isDownloading, fileStatus.downloadProgress < 1 {
- self.addActivityIndicator(with: Float(fileStatus.downloadProgress))
- }
- }
- }
- if let contactImage = message.file().contactPhotoImage() {
- filePreviewImageView.image = contactImage
- }
- }
- func prepareForReuseFileCell() {
- self.filePreviewImageView?.cancelImageDownloadTask()
- self.filePreviewImageView?.layer.borderWidth = 0
- self.filePreviewImageView?.image = nil
- self.filePreviewPlayIconImageView?.isHidden = true
- self.clearFileStatusView()
- }
- // MARK: - Preview
- func requestPreview(for message: NCChatMessage, with account: TalkAccount) {
- // Don't request a preview if we know that there's none
- guard let file = message.file(), file.previewAvailable else {
- self.showFallbackIcon(for: message)
- return
- }
- // In case we can determine the height before requesting the preview, adjust the imageView constraints accordingly
- if file.previewImageHeight > 0 {
- self.filePreviewImageViewHeightConstraint?.constant = CGFloat(file.previewImageHeight)
- } else {
- let estimatedPreviewHeight = BaseChatTableViewCell.getEstimatedPreviewSize(for: message)
- if estimatedPreviewHeight > 0 {
- self.filePreviewImageViewHeightConstraint?.constant = estimatedPreviewHeight
- }
- }
- self.filePreviewActivityIndicator?.isHidden = false
- self.filePreviewActivityIndicator?.startAnimating()
- if message.isAnimatableGif {
- self.requestGifPreview(for: message, with: account)
- } else {
- self.requestDefaultPreview(for: message, with: account)
- }
- }
- func requestGifPreview(for message: NCChatMessage, with account: TalkAccount) {
- guard let fileId = message.file()?.parameterId else { return }
- let fileControllerWrapper = NCChatFileControllerWrapper()
- self.fileControllerWrapper = fileControllerWrapper
- fileControllerWrapper.downloadFile(withFileId: fileId) { fileLocalPath in
- // Check if we are still on the same cell
- guard let cellMessage = self.message, let imageView = self.filePreviewImageView, cellMessage.file().parameterId == fileId
- else {
- // Different cell, don't do anything
- return
- }
- guard let fileLocalPath, let data = try? Data(contentsOf: URL(fileURLWithPath: fileLocalPath)),
- let gifImage = try? UIImage(gifData: data), let baseImage = UIImage(data: data) else {
- // No gif, try to request a normal preview
- self.requestDefaultPreview(for: message, with: account)
- return
- }
- imageView.setGifImage(gifImage)
- self.adjustImageView(toImageSize: baseImage, ofMessage: message)
- }
- }
- func requestDefaultPreview(for message: NCChatMessage, with account: TalkAccount) {
- guard let file = message.file() else { return }
- let requestedHeight = Int(3 * fileMessageCellFileMaxPreviewHeight)
- guard let previewRequest = NCAPIController.sharedInstance().createPreviewRequest(forFile: file.parameterId, withMaxHeight: requestedHeight, using: account) else { return }
- self.filePreviewImageView?.setImageWith(previewRequest, placeholderImage: nil, success: { [weak self] _, _, image in
- guard let self, let imageView = self.filePreviewImageView else { return }
- imageView.image = image
- self.adjustImageView(toImageSize: image, ofMessage: message)
- }, failure: { _, _, _ in
- self.showFallbackIcon(for: message)
- })
- }
- func adjustImageView(toImageSize image: UIImage, ofMessage message: NCChatMessage) {
- guard let imageView = self.filePreviewImageView, let file = message.file() else { return }
- let isVideoFile = NCUtils.isVideo(fileType: file.mimetype)
- let isMediaFile = isVideoFile || NCUtils.isImage(fileType: file.mimetype)
- self.filePreviewActivityIndicator?.isHidden = true
- self.filePreviewActivityIndicator?.stopAnimating()
- let imageSize = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)
- let previewSize = BaseChatTableViewCell.getPreviewSize(from: imageSize, isMediaFile)
- if !previewSize.width.isFinite || !previewSize.height.isFinite {
- self.showFallbackIcon(for: message)
- return
- }
- imageView.layer.borderColor = UIColor.secondarySystemFill.cgColor
- imageView.layer.borderWidth = 1
- self.filePreviewImageViewHeightConstraint?.constant = previewSize.height
- self.filePreviewImageViewWidthConstraint?.constant = previewSize.width
- if isVideoFile {
- // only show the play icon if there is an image preview (not on top of the default video placeholder)
- self.filePreviewPlayIconImageView?.isHidden = false
- // if the video preview is very narrow, make the play icon fit inside
- self.filePreviewPlayIconImageView?.frame = CGRect(x: 0, y: 0, width: min(min(previewSize.height, previewSize.width), fileMessageCellVideoPlayIconSize), height: min(min(previewSize.height, previewSize.width), fileMessageCellVideoPlayIconSize))
- self.filePreviewPlayIconImageView?.center = CGPoint(x: previewSize.width / 2.0, y: previewSize.height / 2.0)
- }
- self.delegate?.cellHasDownloadedImagePreview(withHeight: ceil(previewSize.height), for: message)
- }
- func showFallbackIcon(for message: NCChatMessage) {
- let imageName = NCUtils.previewImage(forMimeType: message.file().mimetype)
- if let image = UIImage(named: imageName) {
- let size = CGSize(width: fileMessageCellFileMaxPreviewWidth, height: fileMessageCellFileMaxPreviewHeight)
- if let renderedImage = NCUtils.renderAspectImage(image: image, ofSize: size, centerImage: false) {
- self.filePreviewImageView?.image = renderedImage
- self.filePreviewImageViewHeightConstraint?.constant = renderedImage.size.height
- self.filePreviewImageViewWidthConstraint?.constant = renderedImage.size.width
- }
- }
- self.filePreviewActivityIndicator?.isHidden = true
- self.filePreviewActivityIndicator?.stopAnimating()
- }
- @objc
- func filePreviewTapped() {
- guard let message = self.message,
- let fileParameter = message.file(),
- fileParameter.path != nil, fileParameter.link != nil
- else { return }
- self.delegate?.cellWants(toDownloadFile: fileParameter, for: message)
- }
- // MARK: - Preview height calculation
- static func getPreviewSize(from imageSize: CGSize, _ isMediaFile: Bool) -> CGSize {
- var width = imageSize.width
- var height = imageSize.height
- let previewMaxHeight = isMediaFile ? fileMessageCellMediaFilePreviewHeight : fileMessageCellFileMaxPreviewHeight
- let previewMaxWidth = isMediaFile ? fileMessageCellMediaFileMaxPreviewWidth : fileMessageCellFileMaxPreviewWidth
- if height < fileMessageCellMinimumHeight {
- let ratio = fileMessageCellMinimumHeight / height
- width *= ratio
- if width > previewMaxWidth {
- width = previewMaxWidth
- }
- height = fileMessageCellMinimumHeight
- } else {
- if height > previewMaxHeight {
- let ratio = previewMaxHeight / height
- width *= ratio
- height = previewMaxHeight
- }
- if width > previewMaxWidth {
- let ratio = previewMaxWidth / width
- width = previewMaxWidth
- height *= ratio
- }
- }
- return CGSize(width: width, height: height)
- }
- static func getEstimatedPreviewSize(for message: NCChatMessage?) -> CGFloat {
- guard let message, let fileParameter = message.file() else { return 0 }
- // We don't have any information about the image to display
- if fileParameter.width == 0 && fileParameter.height == 0 {
- return 0
- }
- // We can only estimate the height for images and videos
- if !NCUtils.isVideo(fileType: fileParameter.mimetype), !NCUtils.isImage(fileType: fileParameter.mimetype) {
- return 0
- }
- let imageSize = CGSize(width: CGFloat(fileParameter.width), height: CGFloat(fileParameter.height))
- let previewSize = self.getPreviewSize(from: imageSize, true)
- return ceil(previewSize.height)
- }
- }
|