Skip to content

Commit d007471

Browse files
authored
Add Support Ticket Attachments (#24972)
* Add video support to AsyncImageKit * Introduce+adopt WordPressCoreProtocols * Add ticket attachment support * UI fixes * Locally summarize bot conversation titles summarization Use first message for conversation title * Add support conversation status support conversation status status * Add attachment previews for video + PDFs * Disable rich text * Add maximum upload size Add maximum upload size * Add network debugging * Fix dependency warning * Allow screen recordings and screenshots * Fix cancel button placement * Address suggestions * Fix AsyncImage video support * Use ContentUnavailableView * Add localization * Fix up EmptyDiskCacheView * Fix a library issue * Fix some missed localizations * Handle video loading errors * Fix a missed localized string * Move support-specific extension into support
1 parent 66cd45f commit d007471

40 files changed

+2265
-710
lines changed

Modules/Package.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ let package = Package(
2121
.library(name: "WordPressShared", targets: ["WordPressShared"]),
2222
.library(name: "WordPressUI", targets: ["WordPressUI"]),
2323
.library(name: "WordPressReader", targets: ["WordPressReader"]),
24+
.library(name: "WordPressCore", targets: ["WordPressCore"]),
25+
.library(name: "WordPressCoreProtocols", targets: ["WordPressCoreProtocols"]),
2426
],
2527
dependencies: [
2628
.package(url: "https://github.com/airbnb/lottie-ios", from: "4.4.0"),
@@ -137,7 +139,7 @@ let package = Package(
137139
name: "Support",
138140
dependencies: [
139141
"AsyncImageKit",
140-
"WordPressCore",
142+
"WordPressCoreProtocols",
141143
]
142144
),
143145
.target(name: "TextBundle"),
@@ -152,10 +154,15 @@ let package = Package(
152154
], swiftSettings: [.swiftLanguageMode(.v5)]),
153155
.target(name: "WordPressFlux", swiftSettings: [.swiftLanguageMode(.v5)]),
154156
.target(name: "WordPressCore", dependencies: [
157+
"WordPressCoreProtocols",
155158
"WordPressShared",
156-
.product(name: "WordPressAPI", package: "wordpress-rs")
159+
.product(name: "WordPressAPI", package: "wordpress-rs"),
157160
]
158161
),
162+
.target(name: "WordPressCoreProtocols", dependencies: [
163+
// This package should never have dependencies – it exists to expose protocols implemented in WordPressCore
164+
// to UI code, because `wordpress-rs` doesn't work nicely with previews.
165+
]),
159166
.target(name: "WordPressLegacy", dependencies: ["DesignSystem", "WordPressShared"]),
160167
.target(name: "WordPressSharedObjC", resources: [.process("Resources")], swiftSettings: [.swiftLanguageMode(.v5)]),
161168
.target(
@@ -177,6 +184,7 @@ let package = Package(
177184
"DesignSystem",
178185
"WordPressShared",
179186
"WordPressLegacy",
187+
.product(name: "ColorStudio", package: "color-studio"),
180188
.product(name: "Reachability", package: "Reachability"),
181189
],
182190
resources: [.process("Resources")],

Modules/Sources/Support/Extensions/Foundation.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ extension Date {
55
let calendar = Calendar.autoupdatingCurrent
66
return calendar.isDateInToday(self)
77
}
8+
9+
var hasPast: Bool {
10+
Date.now > self
11+
}
12+
}
13+
14+
extension String {
15+
func applyingNumericMorphology(for number: Int) -> String {
16+
var attr = AttributedString(self)
17+
var morphology = Morphology()
18+
morphology.number = switch number {
19+
case 0: .zero
20+
case 1: .singular
21+
case 2: .pluralTwo
22+
case 3...7: .pluralFew
23+
case 7...: .pluralMany
24+
default: .plural
25+
}
26+
attr.inflect = InflectionRule(morphology: morphology)
27+
28+
return attr.inflected().characters.reduce(into: "") { $0.append($1) }
29+
}
830
}
931

1032
extension AttributedString {
@@ -93,4 +115,15 @@ extension Task where Failure == Error {
93115
return try await MainActor.run(body: operation)
94116
}
95117
}
118+
119+
static func runForAtLeast<C>(
120+
_ duration: C.Instant.Duration,
121+
operation: @escaping @Sendable () async throws -> Success,
122+
clock: C = .continuous
123+
) async throws -> Success where C: Clock {
124+
async let waitResult: () = try await clock.sleep(for: duration)
125+
async let performTask = try await operation()
126+
127+
return try await (waitResult, performTask).1
128+
}
96129
}

Modules/Sources/Support/InternalDataProvider.swift

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import Foundation
2-
import WordPressCore
2+
import AVFoundation
3+
import AsyncImageKit
4+
import WordPressCoreProtocols
35

46
// This file is all module-internal and provides sample data for UI development
57

@@ -8,7 +10,9 @@ extension SupportDataProvider {
810
applicationLogProvider: InternalLogDataProvider(),
911
botConversationDataProvider: InternalBotConversationDataProvider(),
1012
userDataProvider: InternalUserDataProvider(),
11-
supportConversationDataProvider: InternalSupportConversationDataProvider()
13+
supportConversationDataProvider: InternalSupportConversationDataProvider(),
14+
diagnosticsDataProvider: InternalDiagnosticsDataProvider(),
15+
mediaHost: InternalMediaHost()
1216
)
1317

1418
static let applicationLog = ApplicationLog(path: URL(filePath: #filePath), createdAt: Date(), modifiedAt: Date())
@@ -21,6 +25,7 @@ extension SupportDataProvider {
2125
static let botConversation = BotConversation(
2226
id: 1234,
2327
title: "App Crashing on Launch",
28+
createdAt: Date().addingTimeInterval(-3600), // 1 hour ago
2429
messages: [
2530
BotMessage(
2631
id: 1001,
@@ -84,6 +89,7 @@ extension SupportDataProvider {
8489
BotConversation(
8590
id: 5678,
8691
title: "App Crashing on Launch",
92+
createdAt: Date().addingTimeInterval(-60), // 1 minute ago
8793
messages: botConversation.messages + [
8894
BotMessage(
8995
id: 1009,
@@ -107,48 +113,56 @@ extension SupportDataProvider {
107113
id: 1,
108114
title: "Login Issues with Two-Factor Authentication",
109115
description: "I'm having trouble logging into my account. The two-factor authentication code isn't working properly and I keep getting locked out.",
116+
status: .waitingForSupport,
110117
lastMessageSentAt: Date().addingTimeInterval(-300) // 5 minutes ago
111118
),
112119
ConversationSummary(
113120
id: 2,
114121
title: "Billing Question - Duplicate Charges",
115122
description: "I noticed duplicate charges on my credit card statement for this month's subscription. Can you help me understand what happened?",
123+
status: .waitingForUser,
116124
lastMessageSentAt: Date().addingTimeInterval(-3600) // 1 hour ago
117125
),
118126
ConversationSummary(
119127
id: 3,
120128
title: "Feature Request: Dark Mode Support",
121129
description: "Would it be possible to add dark mode support to the mobile app? Many users in our team have been requesting this feature.",
130+
status: .resolved,
122131
lastMessageSentAt: Date().addingTimeInterval(-86400) // 1 day ago
123132
),
124133
ConversationSummary(
125134
id: 4,
126135
title: "Data Export Not Working",
127136
description: "I'm trying to export my data but the process keeps failing at 50%. Is there a known issue with large datasets?",
137+
status: .resolved,
128138
lastMessageSentAt: Date().addingTimeInterval(-172800) // 2 days ago
129139
),
130140
ConversationSummary(
131141
id: 5,
132142
title: "Account Migration Assistance",
133143
description: "I need help migrating my old account to the new system. I have several years of data that I don't want to lose.",
144+
status: .resolved,
134145
lastMessageSentAt: Date().addingTimeInterval(-259200) // 3 days ago
135146
),
136147
ConversationSummary(
137148
id: 6,
138149
title: "API Rate Limiting Questions",
139150
description: "Our application is hitting rate limits frequently. Can we discuss increasing our API quota or optimizing our usage patterns?",
151+
status: .closed,
140152
lastMessageSentAt: Date().addingTimeInterval(-604800) // 1 week ago
141153
),
142154
ConversationSummary(
143155
id: 7,
144156
title: "Security Concern - Suspicious Activity",
145157
description: "I received an email about suspicious activity on my account. I want to make sure my account is secure and review recent access logs.",
158+
status: .closed,
146159
lastMessageSentAt: Date().addingTimeInterval(-1209600) // 2 weeks ago
147160
),
148161
ConversationSummary(
149162
id: 8,
150163
title: "Integration Help with Webhook Setup",
151164
description: "I'm having trouble setting up webhooks for our CRM integration. The endpoints aren't receiving the expected payload format.",
165+
status: .closed,
152166
lastMessageSentAt: Date().addingTimeInterval(-1814400) // 3 weeks ago
153167
)
154168
]
@@ -158,6 +172,7 @@ extension SupportDataProvider {
158172
title: "Issue with app crashes",
159173
description: "The app keeps crashing when I try to upload photos. This has been happening for the past week and is very frustrating.",
160174
lastMessageSentAt: Date().addingTimeInterval(-2400),
175+
status: .closed,
161176
messages: [
162177
Message(
163178
id: 1,
@@ -198,7 +213,15 @@ extension SupportDataProvider {
198213
createdAt: Date().addingTimeInterval(-1800),
199214
authorName: "Test User",
200215
authorIsUser: true,
201-
attachments: []
216+
attachments: [
217+
Attachment(
218+
id: 1234,
219+
filename: "sample-1234.jpg",
220+
contentType: "application/jpeg",
221+
fileSize: 1234,
222+
url: URL(string: "https://picsum.photos/seed/1/800/600")!
223+
)
224+
]
202225
),
203226
Message(
204227
id: 6,
@@ -321,6 +344,8 @@ actor InternalUserDataProvider: CurrentUserDataProvider {
321344
}
322345

323346
actor InternalSupportConversationDataProvider: SupportConversationDataProvider {
347+
let maximumUploadSize: UInt64 = 5_000_000 // 5MB
348+
324349
private var conversations: [UInt64: Conversation] = [:]
325350

326351
nonisolated func loadSupportConversations() throws -> any CachedAndFetchedResult<[ConversationSummary]> {
@@ -374,6 +399,7 @@ actor InternalSupportConversationDataProvider: SupportConversationDataProvider {
374399
title: subject,
375400
description: message,
376401
lastMessageSentAt: Date(),
402+
status: .waitingForSupport,
377403
messages: [Message(
378404
id: 1234,
379405
content: message,
@@ -389,3 +415,51 @@ actor InternalSupportConversationDataProvider: SupportConversationDataProvider {
389415
self.conversations[value.id] = value
390416
}
391417
}
418+
419+
actor InternalDiagnosticsDataProvider: DiagnosticsDataProvider {
420+
421+
private var didClear: Bool = false
422+
423+
func fetchDiskCacheUsage() async throws -> WordPressCoreProtocols.DiskCacheUsage {
424+
if didClear {
425+
DiskCacheUsage(fileCount: 0, byteCount: 0)
426+
} else {
427+
DiskCacheUsage(fileCount: 64, byteCount: 623_423_562)
428+
}
429+
}
430+
431+
func clearDiskCache(progress: @Sendable (CacheDeletionProgress) async throws -> Void) async throws {
432+
let totalFiles = 12
433+
434+
// Initial progress (0%)
435+
try await progress(CacheDeletionProgress(filesDeleted: 0, totalFileCount: totalFiles))
436+
437+
for i in 1...totalFiles {
438+
// Pretend each file takes a short time to delete
439+
try await Task.sleep(for: .milliseconds(150))
440+
441+
// Report incremental progress
442+
try await progress(CacheDeletionProgress(filesDeleted: i, totalFileCount: totalFiles))
443+
}
444+
445+
self.didClear = true
446+
}
447+
}
448+
449+
actor InternalMediaHost: MediaHostProtocol {
450+
func authenticatedRequest(for url: URL) async throws -> URLRequest {
451+
if Bool.random() {
452+
throw CocoaError(.coderInvalidValue)
453+
}
454+
455+
return URLRequest(url: url)
456+
}
457+
458+
func authenticatedAsset(for url: URL) async throws -> AVURLAsset {
459+
if Bool.random() {
460+
throw CocoaError(.coderInvalidValue)
461+
}
462+
463+
return AVURLAsset(url: url)
464+
}
465+
}

0 commit comments

Comments
 (0)