Skip to content

Commit 61ede5e

Browse files
authored
Redesign attachment uploading and error states (#1408)
* Update uploading progress indicator for attachements * Refactor into one view * Fix loading spinner not shown for videos * Add retry icon view * Retry sending message when tapping retry badge view * Fix shimerring effect when attachment fails to load * Remove anused code * Create seperate file for RetryBadgeView. * Refactor RetryBadgeView * Add test coverage * Re-record snapshots * Upload file attachments uploading and error states * Reuse FileAttachmentDisplayView * Update changelog for attachment state redesign * Remove extra changelog entries * Remove redundant snapshot tests * Fix SwiftFormat indentation in LoadingSpinnerView
1 parent 2f6d88d commit 61ede5e

34 files changed

Lines changed: 401 additions & 199 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
44
# Upcoming
55

66
### ✅ Added
7+
- Redesign attachment uploading progress and error state indicators [#1408](https://github.com/GetStream/stream-chat-swiftui/pull/1408)
8+
- Add inline upload progress and retry UI for file attachments [#1408](https://github.com/GetStream/stream-chat-swiftui/pull/1408)
9+
- Add `RetryBadgeView` for failed uploads and thumbnail loads [#1408](https://github.com/GetStream/stream-chat-swiftui/pull/1408)
710
- Add `ComposerConfig.isVoiceRecordingAutoSendEnabled` to support sending a recording instantly on release [#1362](https://github.com/GetStream/stream-chat-swiftui/pull/1362)
811
- Redesign `JumpToUnreadButton` [#1351](https://github.com/GetStream/stream-chat-swiftui/pull/1351)
912
- Show deleted messages in channel list preview [#1338](https://github.com/GetStream/stream-chat-swiftui/pull/1338)

Sources/StreamChatSwiftUI/ChatChannel/ChannelInfo/FileAttachmentsView.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public struct FileAttachmentsView: View {
3939
ScrollView {
4040
LazyVStack(spacing: 0) {
4141
ForEach(viewModel.attachmentsDataSource) { monthlyDataSource in
42-
ForEach(monthlyDataSource.attachments, id: \.self) { attachment in
42+
ForEach(monthlyDataSource.attachments) { attachment in
4343
let url = attachment.assetURL
4444

4545
Button {
@@ -62,7 +62,6 @@ public struct FileAttachmentsView: View {
6262
}
6363
.padding(.vertical, tokens.spacingSm)
6464
.padding(.horizontal)
65-
.withDownloadingStateIndicator(for: attachment.downloadingState, url: attachment.assetURL)
6665
.sheet(item: $viewModel.selectedAttachment) { item in
6766
FileAttachmentPreview(attachment: item)
6867
}

Sources/StreamChatSwiftUI/ChatMessageList/AttachmentDownloadingStateView.swift

Lines changed: 0 additions & 66 deletions
This file was deleted.

Sources/StreamChatSwiftUI/ChatMessageList/AttachmentUploadingStateView.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import SwiftUI
99
struct AttachmentUploadingStateView: View {
1010
@Injected(\.images) private var images
1111
@Injected(\.colors) private var colors
12-
@Injected(\.fonts) private var fonts
1312

1413
var uploadState: AttachmentUploadingState
1514
var url: URL
@@ -18,19 +17,21 @@ struct AttachmentUploadingStateView: View {
1817
Group {
1918
switch uploadState.state {
2019
case let .uploading(progress: progress):
21-
BottomRightView {
22-
PercentageProgressView(progress: progress)
20+
ZStack {
21+
Color(colors.backgroundCoreOverlayLight)
22+
LoadingSpinnerView(
23+
size: LoadingSpinnerSize.medium,
24+
progress: Double(progress)
25+
)
2326
}
27+
.allowsHitTesting(false)
2428

2529
case .uploadingFailed:
26-
BottomRightView {
27-
Image(uiImage: images.messageListErrorIndicator)
28-
.renderingMode(.template)
29-
.foregroundColor(Color(colors.badgeBackgroundError))
30-
.background(Color.white)
31-
.clipShape(Circle())
32-
.offset(x: -4, y: -4)
30+
ZStack {
31+
Color(colors.backgroundCoreOverlayLight)
32+
RetryBadgeView()
3333
}
34+
.allowsHitTesting(false)
3435

3536
case .uploaded:
3637
TopRightView {

Sources/StreamChatSwiftUI/ChatMessageList/FileAttachmentView.swift

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ public struct FileAttachmentsContainer<Factory: ViewFactory>: View {
4848
}
4949

5050
public struct FileAttachmentView: View {
51-
@Injected(\.utils) private var utils
52-
@Injected(\.images) private var images
53-
@Injected(\.fonts) private var fonts
54-
@Injected(\.colors) private var colors
5551
@Injected(\.tokens) private var tokens
5652
@Injected(\.chatClient) private var chatClient
5753

@@ -72,26 +68,37 @@ public struct FileAttachmentView: View {
7268
FileAttachmentDisplayView(
7369
url: attachment.assetURL,
7470
title: attachment.title ?? "",
75-
sizeString: attachment.file.sizeString
71+
sizeString: attachment.file.sizeString,
72+
uploadingState: attachment.uploadingState,
73+
onRetry: { retryUpload() }
7674
)
7775
.onTapGesture {
78-
fullScreenShown = true
76+
if attachment.uploadingState?.state != .uploadingFailed {
77+
fullScreenShown = true
78+
}
7979
}
8080
.accessibilityAction {
81-
fullScreenShown = true
81+
if attachment.uploadingState?.state != .uploadingFailed {
82+
fullScreenShown = true
83+
}
8284
}
8385

8486
Spacer()
8587
}
8688
.padding(.all, tokens.spacingSm)
8789
.frame(width: width)
88-
.withUploadingStateIndicator(for: attachment.uploadingState, url: attachment.assetURL)
89-
.withDownloadingStateIndicator(for: attachment.downloadingState, url: attachment.assetURL)
9090
.sheet(isPresented: $fullScreenShown) {
9191
FileAttachmentPreview(attachment: attachment)
9292
}
9393
.accessibilityIdentifier("FileAttachmentView")
9494
}
95+
96+
private func retryUpload() {
97+
let messageId = attachment.id.messageId
98+
let cid = attachment.id.cid
99+
let controller = chatClient.messageController(cid: cid, messageId: messageId)
100+
controller.resendMessage()
101+
}
95102
}
96103

97104
public struct FileAttachmentDisplayView: View {
@@ -103,11 +110,21 @@ public struct FileAttachmentDisplayView: View {
103110
var url: URL
104111
var title: String
105112
var sizeString: String
113+
var uploadingState: AttachmentUploadingState?
114+
var onRetry: (() -> Void)?
106115

107-
public init(url: URL, title: String, sizeString: String) {
116+
public init(
117+
url: URL,
118+
title: String,
119+
sizeString: String,
120+
uploadingState: AttachmentUploadingState? = nil,
121+
onRetry: (() -> Void)? = nil
122+
) {
108123
self.url = url
109124
self.title = title
110125
self.sizeString = sizeString
126+
self.uploadingState = uploadingState
127+
self.onRetry = onRetry
111128
}
112129

113130
public var body: some View {
@@ -122,20 +139,87 @@ public struct FileAttachmentDisplayView: View {
122139
.font(fonts.body)
123140
.lineLimit(1)
124141
.foregroundColor(Color(colors.textPrimary))
125-
Text(sizeString)
126-
.font(fonts.subheadline)
127-
.lineLimit(1)
128-
.foregroundColor(Color(colors.textTertiary))
142+
subtitleContent
129143
}
130144
Spacer()
131145
}
132146
.accessibilityElement(children: .combine)
133147
}
134148

149+
// MARK: - Private
150+
151+
@ViewBuilder
152+
private var subtitleContent: some View {
153+
if let uploadingState {
154+
switch uploadingState.state {
155+
case let .uploading(progress):
156+
uploadingSubtitle(progress: progress, file: uploadingState.file)
157+
case .uploadingFailed:
158+
uploadFailedSubtitle
159+
default:
160+
fileSizeText
161+
}
162+
} else {
163+
fileSizeText
164+
}
165+
}
166+
167+
private func uploadingSubtitle(progress: Double, file: AttachmentFile) -> some View {
168+
HStack(spacing: tokens.spacingXxs) {
169+
LoadingSpinnerView(
170+
size: LoadingSpinnerSize.extraSmall,
171+
progress: Double(progress)
172+
)
173+
Text(Self.uploadProgressText(progress: progress, file: file))
174+
.font(fonts.subheadline)
175+
.lineLimit(1)
176+
.foregroundColor(Color(colors.textSecondary))
177+
}
178+
}
179+
180+
private var uploadFailedSubtitle: some View {
181+
VStack(alignment: .leading, spacing: tokens.spacingXxxs) {
182+
HStack(spacing: tokens.spacingXxs) {
183+
Image(systemName: "exclamationmark.triangle.fill")
184+
.resizable()
185+
.scaledToFit()
186+
.frame(width: tokens.iconSizeSm, height: tokens.iconSizeSm)
187+
.foregroundColor(Color(colors.accentError))
188+
Text(L10n.Message.Sending.attachmentUploadFailed)
189+
.font(fonts.subheadline)
190+
.lineLimit(1)
191+
.foregroundColor(Color(colors.textSecondary))
192+
}
193+
if let onRetry {
194+
Button(action: onRetry) {
195+
Text(L10n.Message.Sending.attachmentRetryUpload)
196+
.font(fonts.subheadline)
197+
.foregroundColor(Color(colors.textLink))
198+
}
199+
.buttonStyle(.plain)
200+
}
201+
}
202+
}
203+
204+
private var fileSizeText: some View {
205+
Text(sizeString)
206+
.font(fonts.subheadline)
207+
.lineLimit(1)
208+
.foregroundColor(Color(colors.textTertiary))
209+
}
210+
135211
private var previewImage: UIImage {
136212
let iconName = url.pathExtension
137213
return images.fileIconPreviews[iconName] ?? images.iconOther
138214
}
215+
216+
static func uploadProgressText(progress: Double, file: AttachmentFile) -> String {
217+
let formatter = AttachmentFile.sizeFormatter
218+
let uploaded = Int64(progress * Double(file.size))
219+
let uploadedText = formatter.string(fromByteCount: uploaded)
220+
let totalText = formatter.string(fromByteCount: file.size)
221+
return "\(uploadedText) / \(totalText)"
222+
}
139223
}
140224

141225
struct DownloadShareAttachmentView<Payload: DownloadableAttachmentPayload>: View {

0 commit comments

Comments
 (0)