Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ class GiphyPickerViewController: UIViewController {
return searchController
}()

private lazy var waterfallLayout: GiphyWaterfallLayout = {
let layout = GiphyWaterfallLayout()
private lazy var waterfallLayout: WaterfallLayout = {
let layout = WaterfallLayout()
layout.delegate = self
return layout
}()
Expand Down Expand Up @@ -432,13 +432,13 @@ extension GiphyPickerViewController: UICollectionViewDataSource, UICollectionVie
}
}

// MARK: - GiphyWaterfallLayoutDelegate
// MARK: - WaterfallLayoutDelegate

extension GiphyPickerViewController: GiphyWaterfallLayoutDelegate {
extension GiphyPickerViewController: WaterfallLayoutDelegate {

func waterfallLayout(_ layout: GiphyWaterfallLayout, aspectRatioForItemAt index: Int) -> CGFloat {
guard index < self.items.count else { return Self.fallbackAspectRatio }
func waterfallLayout(_ layout: WaterfallLayout, aspectRatioForItemAt indexPath: IndexPath) -> CGFloat {
guard indexPath.item < self.items.count else { return Self.fallbackAspectRatio }

return self.items[index].aspectRatio
return self.items[indexPath.item].aspectRatio
}
}
120 changes: 0 additions & 120 deletions NextcloudTalk/Chat/Chat views/Giphy/GiphyWaterfallLayout.swift

This file was deleted.

48 changes: 48 additions & 0 deletions NextcloudTalk/Chat/ChatFileDownloadObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//

import Foundation

/// Follows the download of one file through the notifications NCChatFileController posts
class ChatFileDownloadObserver {

enum Change {
case isDownloading(Bool)
/// nil while the size of the download is unknown
case progress(Float?)
}

var fileParameter: NCMessageFileParameter?

private let onChange: (Change) -> Void
private var observers: [NSObjectProtocol] = []

init(onChange: @escaping (Change) -> Void) {
self.onChange = onChange

// Download progress can be reported off the main queue
self.observers.append(NotificationCenter.default.addObserver(forName: .NCChatFileControllerDidChangeIsDownloading, object: nil, queue: .main) { [weak self] notification in
guard let status = self?.status(from: notification) else { return }

self?.onChange(.isDownloading(status.isDownloading))
})

self.observers.append(NotificationCenter.default.addObserver(forName: .NCChatFileControllerDidChangeDownloadProgress, object: nil, queue: .main) { [weak self] notification in
guard let status = self?.status(from: notification) else { return }

self?.onChange(.progress(status.canReportProgress ? Float(status.downloadProgress) : nil))
})
}

deinit {
self.observers.forEach { NotificationCenter.default.removeObserver($0) }
}

private func status(from notification: Notification) -> NCChatFileStatus? {
guard let fileParameter = self.fileParameter else { return nil }

return NCChatFileStatus.getStatus(from: notification, for: fileParameter)
}
}
21 changes: 10 additions & 11 deletions NextcloudTalk/Media Viewer/NCMediaViewerPageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ import SwiftyGif
return videoPlayButton
}()

private lazy var downloadObserver = ChatFileDownloadObserver { [weak self] change in
self?.downloadDidChange(change)
}

init(message: NCChatMessage, account: TalkAccount) {
self.message = message
self.account = account
Expand Down Expand Up @@ -193,7 +197,7 @@ import SwiftyGif

self.navigationItem.title = self.message.file()?.name

NotificationCenter.default.addObserver(self, selector: #selector(didChangeDownloadProgress(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeDownloadProgress, object: nil)
self.downloadObserver.fileParameter = self.message.file()

self.startLoading()

Expand Down Expand Up @@ -662,17 +666,12 @@ import SwiftyGif
}
}

func didChangeDownloadProgress(notification: Notification) {
DispatchQueue.main.async {
guard let fileParameter = self.message.file(),
let receivedStatus = NCChatFileStatus.getStatus(from: notification, for: fileParameter),
receivedStatus.canReportProgress
else { return }
private func downloadDidChange(_ change: ChatFileDownloadObserver.Change) {
guard case .progress(let progress?) = change else { return }

self.activityIndicator.indicatorMode = .determinate
self.activityIndicator.setProgress(Float(receivedStatus.downloadProgress), animated: true)
self.downloadIndicator.setProgress(Float(receivedStatus.downloadProgress))
}
self.activityIndicator.indicatorMode = .determinate
self.activityIndicator.setProgress(progress, animated: true)
self.downloadIndicator.setProgress(progress)
}

// MARK: - NCChatFileController delegate
Expand Down
79 changes: 60 additions & 19 deletions NextcloudTalk/Media Viewer/NCMediaViewerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
import Foundation
import UIKit

/// Decides which media the viewer swipes to, instead of the chat messages stored for the room
@objc protocol MediaViewerMessageSource: AnyObject {
/// The closest older message to show before `message`, or nil to stop there
func mediaViewerMessage(before message: NCChatMessage) -> NCChatMessage?

/// The closest newer message to show after `message`, or nil to stop there
func mediaViewerMessage(after message: NCChatMessage) -> NCChatMessage?
}

@objcMembers class NCMediaViewerViewController: UIViewController,
UIPageViewControllerDelegate,
UIPageViewControllerDataSource,
Expand All @@ -15,6 +24,10 @@ import UIKit
private let account: TalkAccount
private let pageController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
private var initialMessage: NCChatMessage
private weak var messageSource: MediaViewerMessageSource?
// Resetting the data source while a swipe animates could disturb it, so that waits for the swipe to end
private var isTransitioning = false
private var needsAdjacentPagesReload = false

private lazy var shareButton = {
let shareButton = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
Expand Down Expand Up @@ -72,10 +85,11 @@ import UIKit
return showMessageButton
}()

init(initialMessage: NCChatMessage, room: NCRoom, account: TalkAccount) {
init(initialMessage: NCChatMessage, room: NCRoom, account: TalkAccount, messageSource: MediaViewerMessageSource? = nil) {
self.room = room
self.initialMessage = initialMessage
self.account = account
self.messageSource = messageSource

super.init(nibName: nil, bundle: nil)
}
Expand Down Expand Up @@ -152,6 +166,20 @@ import UIKit
return self.pageController.viewControllers?.first as? NCMediaViewerPageViewController
}

/// For a message source that got more messages after it reported the end
func reloadAdjacentPages() {
guard !self.isTransitioning else {
self.needsAdjacentPagesReload = true
return
}

self.needsAdjacentPagesReload = false

// The page controller keeps a nil neighbour until its data source is set again, which leaves the current page alone
self.pageController.dataSource = nil
self.pageController.dataSource = self
}

// MARK: - PageViewController delegate

func getAllFileMessages() -> RLMResults<AnyObject>? {
Expand All @@ -163,22 +191,29 @@ import UIKit
return messages
}

/// Whether the viewer can show the file of `message`, formats only VLC can play excluded
static func canShowMedia(of message: NCChatMessage) -> Bool {
guard let file = message.file(), let filePath = file.path else { return false }

let fileType = file.mimetype ?? ""
let isSupportedMedia = NCUtils.isImage(fileType: fileType) || NCUtils.isVideo(fileType: fileType)
let isUnsupportedExtension = VLCKitVideoViewController.supportedFileExtensions.contains(URL(fileURLWithPath: filePath).pathExtension.lowercased())

return isSupportedMedia && !isUnsupportedExtension
}

func getPreviousFileMessage(from message: NCChatMessage) -> NCChatMessage? {
if let messageSource {
return messageSource.mediaViewerMessage(before: message)
}

let prevQuery = NSPredicate(format: "messageId < %ld", message.messageId)

guard let queriedObjects = self.getAllFileMessages()?.objects(with: prevQuery) else { return nil }
let messageObject = queriedObjects.lastObject()

if let message = messageObject as? NCChatMessage {
guard let filePath = message.file().path else {
return self.getPreviousFileMessage(from: message)
}

let fileType = message.file()?.mimetype ?? ""
let isSupportedMedia = NCUtils.isImage(fileType: fileType) || NCUtils.isVideo(fileType: fileType)
let isUnsupportedExtension = VLCKitVideoViewController.supportedFileExtensions.contains(URL(fileURLWithPath: filePath).pathExtension.lowercased())

if isSupportedMedia && !isUnsupportedExtension {
if Self.canShowMedia(of: message) {
return message
}

Expand All @@ -189,20 +224,16 @@ import UIKit
}

func getNextFileMessage(from message: NCChatMessage) -> NCChatMessage? {
if let messageSource {
return messageSource.mediaViewerMessage(after: message)
}

let prevQuery = NSPredicate(format: "messageId > %ld", message.messageId)

guard let messageObject = self.getAllFileMessages()?.objects(with: prevQuery).firstObject() else { return nil }

if let message = messageObject as? NCChatMessage {
guard let filePath = message.file().path else {
return self.getNextFileMessage(from: message)
}

let fileType = message.file()?.mimetype ?? ""
let isSupportedMedia = NCUtils.isImage(fileType: fileType) || NCUtils.isVideo(fileType: fileType)
let isUnsupportedExtension = VLCKitVideoViewController.supportedFileExtensions.contains(URL(fileURLWithPath: filePath).pathExtension.lowercased())

if isSupportedMedia && !isUnsupportedExtension {
if Self.canShowMedia(of: message) {
return message
}

Expand Down Expand Up @@ -232,7 +263,17 @@ import UIKit
return mediaPageViewController
}

func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
self.isTransitioning = true
}

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
self.isTransitioning = false

if self.needsAdjacentPagesReload {
self.reloadAdjacentPages()
}

// Update the titel of the currently shown viewController
guard let mediaPageViewController = self.getCurrentPageViewController() else { return }
self.navigationItem.title = mediaPageViewController.navigationItem.title
Expand Down
Loading
Loading