Skip to content

Commit 2f84db7

Browse files
committed
feat: Show download progress on shared media
Assisted-by: ClaudeCode:claude-opus-5.5 Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
1 parent 8fb63dc commit 2f84db7

4 files changed

Lines changed: 125 additions & 40 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
//
5+
6+
import Foundation
7+
8+
/// Follows the download of one file through the notifications NCChatFileController posts
9+
class ChatFileDownloadObserver {
10+
11+
enum Change {
12+
case isDownloading(Bool)
13+
/// nil while the size of the download is unknown
14+
case progress(Float?)
15+
}
16+
17+
var fileParameter: NCMessageFileParameter?
18+
19+
private let onChange: (Change) -> Void
20+
private var observers: [NSObjectProtocol] = []
21+
22+
init(onChange: @escaping (Change) -> Void) {
23+
self.onChange = onChange
24+
25+
// Download progress can be reported off the main queue
26+
self.observers.append(NotificationCenter.default.addObserver(forName: .NCChatFileControllerDidChangeIsDownloading, object: nil, queue: .main) { [weak self] notification in
27+
guard let status = self?.status(from: notification) else { return }
28+
29+
self?.onChange(.isDownloading(status.isDownloading))
30+
})
31+
32+
self.observers.append(NotificationCenter.default.addObserver(forName: .NCChatFileControllerDidChangeDownloadProgress, object: nil, queue: .main) { [weak self] notification in
33+
guard let status = self?.status(from: notification) else { return }
34+
35+
self?.onChange(.progress(status.canReportProgress ? Float(status.downloadProgress) : nil))
36+
})
37+
}
38+
39+
deinit {
40+
self.observers.forEach { NotificationCenter.default.removeObserver($0) }
41+
}
42+
43+
private func status(from notification: Notification) -> NCChatFileStatus? {
44+
guard let fileParameter = self.fileParameter else { return nil }
45+
46+
return NCChatFileStatus.getStatus(from: notification, for: fileParameter)
47+
}
48+
}

‎NextcloudTalk/Media Viewer/NCMediaViewerPageViewController.swift‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ import SwiftyGif
157157
return videoPlayButton
158158
}()
159159

160+
private lazy var downloadObserver = ChatFileDownloadObserver { [weak self] change in
161+
self?.downloadDidChange(change)
162+
}
163+
160164
init(message: NCChatMessage, account: TalkAccount) {
161165
self.message = message
162166
self.account = account
@@ -193,7 +197,7 @@ import SwiftyGif
193197

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

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

198202
self.startLoading()
199203

@@ -662,17 +666,12 @@ import SwiftyGif
662666
}
663667
}
664668

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

672-
self.activityIndicator.indicatorMode = .determinate
673-
self.activityIndicator.setProgress(Float(receivedStatus.downloadProgress), animated: true)
674-
self.downloadIndicator.setProgress(Float(receivedStatus.downloadProgress))
675-
}
672+
self.activityIndicator.indicatorMode = .determinate
673+
self.activityIndicator.setProgress(progress, animated: true)
674+
self.downloadIndicator.setProgress(progress)
676675
}
677676

678677
// MARK: - NCChatFileController delegate

‎NextcloudTalk/Rooms/SharedItemCells.swift‎

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ import SDWebImage
99

1010
class SharedItemTableViewCell: UITableViewCell {
1111

12-
var fileParameter: NCMessageFileParameter?
12+
var fileParameter: NCMessageFileParameter? {
13+
get { self.downloadObserver.fileParameter }
14+
set { self.downloadObserver.fileParameter = newValue }
15+
}
16+
1317
private var activityIndicator: NCActivityIndicator?
1418

19+
private lazy var downloadObserver = ChatFileDownloadObserver { [weak self] change in
20+
self?.downloadDidChange(change)
21+
}
22+
1523
let titleLabel: UILabel = {
1624
let label = UILabel()
1725
label.font = .preferredFont(forTextStyle: .body)
@@ -32,9 +40,6 @@ class SharedItemTableViewCell: UITableViewCell {
3240

3341
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
3442
super.init(style: style, reuseIdentifier: reuseIdentifier)
35-
36-
NotificationCenter.default.addObserver(self, selector: #selector(didChangeIsDownloading(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeIsDownloading, object: nil)
37-
NotificationCenter.default.addObserver(self, selector: #selector(didChangeDownloadProgress(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeDownloadProgress, object: nil)
3843
}
3944

4045
required init?(coder: NSCoder) {
@@ -61,35 +66,24 @@ class SharedItemTableViewCell: UITableViewCell {
6166
return parts.joined(separator: " ⸱ ")
6267
}
6368

64-
@objc private func didChangeIsDownloading(notification: Notification) {
65-
DispatchQueue.main.async {
66-
guard let fileParameter = self.fileParameter,
67-
let receivedStatus = NCChatFileStatus.getStatus(from: notification, for: fileParameter)
68-
else { return }
69-
70-
if receivedStatus.isDownloading, self.activityIndicator == nil {
71-
self.addActivityIndicator(with: 0)
72-
} else if !receivedStatus.isDownloading, self.activityIndicator != nil {
73-
self.accessoryView = nil
74-
self.activityIndicator = nil
75-
}
76-
}
77-
}
78-
79-
@objc private func didChangeDownloadProgress(notification: Notification) {
80-
DispatchQueue.main.async {
81-
guard let fileParameter = self.fileParameter,
82-
let receivedStatus = NCChatFileStatus.getStatus(from: notification, for: fileParameter)
83-
else { return }
84-
69+
private func downloadDidChange(_ change: ChatFileDownloadObserver.Change) {
70+
switch change {
71+
case .isDownloading(true) where self.activityIndicator == nil:
72+
self.addActivityIndicator(with: 0)
73+
case .isDownloading(false):
74+
self.accessoryView = nil
75+
self.activityIndicator = nil
76+
case .progress(let progress):
8577
if let activityIndicator = self.activityIndicator {
86-
if receivedStatus.canReportProgress {
78+
if let progress {
8779
activityIndicator.indicatorMode = .determinate
88-
activityIndicator.setProgress(Float(receivedStatus.downloadProgress), animated: true)
80+
activityIndicator.setProgress(progress, animated: true)
8981
}
9082
} else {
91-
self.addActivityIndicator(with: Float(receivedStatus.downloadProgress))
83+
self.addActivityIndicator(with: progress ?? 0)
9284
}
85+
default:
86+
break
9387
}
9488
}
9589

‎NextcloudTalk/Rooms/SharedMediaCell.swift‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ class SharedMediaCell: UICollectionViewCell {
1515

1616
private var fileId: String?
1717
private var previewRequest: SDWebImageCombinedOperation?
18+
private var isVideo = false
19+
20+
private lazy var downloadObserver = ChatFileDownloadObserver { [weak self] change in
21+
self?.downloadDidChange(change)
22+
}
23+
24+
private let downloadIndicator: MediaDownloadIndicatorView = {
25+
let downloadIndicator = MediaDownloadIndicatorView(frame: .zero)
26+
downloadIndicator.isHidden = true
27+
return downloadIndicator
28+
}()
1829

1930
private let imageView: UIImageView = {
2031
let imageView = UIImageView()
@@ -68,6 +79,7 @@ class SharedMediaCell: UICollectionViewCell {
6879
self.contentView.addSubview(self.fileIconView)
6980
self.contentView.addSubview(self.playIconView)
7081
self.contentView.addSubview(self.captionView)
82+
self.contentView.addSubview(self.downloadIndicator)
7183
self.captionView.addSubview(self.actorLabel)
7284

7385
for view in [self.imageView, self.fileIconView, self.playIconView, self.captionView, self.actorLabel] {
@@ -90,6 +102,9 @@ class SharedMediaCell: UICollectionViewCell {
90102
self.playIconView.widthAnchor.constraint(equalToConstant: 44),
91103
self.playIconView.heightAnchor.constraint(equalToConstant: 44),
92104

105+
self.downloadIndicator.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor),
106+
self.downloadIndicator.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor),
107+
93108
self.captionView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
94109
self.captionView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
95110
self.captionView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
@@ -128,6 +143,8 @@ class SharedMediaCell: UICollectionViewCell {
128143
self.fileId = nil
129144
self.imageView.image = nil
130145
self.fileIconView.isHidden = true
146+
self.downloadObserver.fileParameter = nil
147+
self.showsDownload(false)
131148
}
132149

133150
func configure(with message: NCChatMessage, aspectRatio: CGFloat, showsServerPreview: Bool, account: TalkAccount) {
@@ -137,7 +154,10 @@ class SharedMediaCell: UICollectionViewCell {
137154
self.fileId = file?.parameterId
138155
self.actorLabel.text = actorDisplayName
139156
self.captionView.isHidden = actorDisplayName.isEmpty
140-
self.playIconView.isHidden = !NCUtils.isVideo(fileType: file?.mimetype ?? "")
157+
self.isVideo = NCUtils.isVideo(fileType: file?.mimetype ?? "")
158+
self.downloadObserver.fileParameter = file
159+
// A download started before this cell showed the file only reports its next change
160+
self.showsDownload(file.flatMap { ChatFileDownloader.shared.downloadStatus(forFileId: $0.parameterId)?.isDownloading } ?? false)
141161
self.accessibilityLabel = [file?.name ?? "", actorDisplayName].filter { !$0.isEmpty }.joined(separator: ", ")
142162

143163
guard let file, file.previewAvailable else {
@@ -170,6 +190,30 @@ class SharedMediaCell: UICollectionViewCell {
170190
}
171191
}
172192

193+
private func downloadDidChange(_ change: ChatFileDownloadObserver.Change) {
194+
switch change {
195+
case .isDownloading(let isDownloading):
196+
self.showsDownload(isDownloading)
197+
case .progress(let progress):
198+
self.showsDownload(true)
199+
200+
if let progress {
201+
self.downloadIndicator.setProgress(progress)
202+
}
203+
}
204+
}
205+
206+
private func showsDownload(_ showsDownload: Bool) {
207+
if showsDownload, self.downloadIndicator.isHidden {
208+
self.downloadIndicator.showProgress()
209+
} else if !showsDownload {
210+
self.downloadIndicator.stopAnimating()
211+
}
212+
213+
self.downloadIndicator.isHidden = !showsDownload
214+
self.playIconView.isHidden = showsDownload || !self.isVideo
215+
}
216+
173217
private func showFileIcon(for file: NCMessageFileParameter?) {
174218
self.imageView.image = nil
175219
self.fileIconView.image = UIImage(named: NCUtils.previewImage(forMimeType: file?.mimetype))

0 commit comments

Comments
 (0)