Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions Tests/KeystoneTests/Tests/Stores/ActivityStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,7 @@ class ActivityServiceRemoteMock: ActivityServiceRemote {

var groupsToReturn: [ActivityGroup]?

override func getActivityForSite(_ siteID: Int,
offset: Int = 0,
count: Int,
after: Date? = nil,
before: Date? = nil,
group: [String] = [],
success: @escaping (_ activities: [Activity], _ hasMore: Bool) -> Void,
failure: @escaping (Error) -> Void) {
override func getActivityForSite(_ siteID: Int, offset: Int = 0, count: Int, after: Date? = nil, before: Date? = nil, group: [String] = [], rewindable: Bool? = nil, searchText: String? = nil, success: @escaping (_ activities: [WordPressKit.Activity], _ hasMore: Bool) -> Void, failure: @escaping (any Error) -> Void) {
getActivityForSiteCalledWithSiteID = siteID
getActivityForSiteCalledWithCount = count
getActivityForSiteCalledWithOffset = offset
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SwiftUI
import WordPressUI
import WordPressKit
import WordPressShared

struct ActivityLogsView: View {
@ObservedObject var viewModel: ActivityLogsViewModel
Expand Down Expand Up @@ -28,6 +29,10 @@ private struct ActivityLogsListView: View {

var body: some View {
List {
if let backupTracker = viewModel.backupTracker {
DownloadableBackupSection(backupTracker: backupTracker)
}

if let response = viewModel.response {
ActivityLogsPaginatedForEach(response: response, blog: viewModel.blog)

Expand Down Expand Up @@ -61,6 +66,9 @@ private struct ActivityLogsListView: View {
.onAppear {
viewModel.onAppear()
}
.onDisappear {
viewModel.onDisappear()
}
.refreshable {
await viewModel.refresh()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ typealias ActivityLogsPaginatedResponse = DataViewPaginatedResponse<ActivityLogR
final class ActivityLogsViewModel: ObservableObject {
let blog: Blog
let isBackupMode: Bool
let backupTracker: DownloadableBackupTracker?

@Published var searchText = ""
@Published var parameters = GetActivityLogsParameters() {
Expand All @@ -31,9 +32,12 @@ final class ActivityLogsViewModel: ObservableObject {
init(blog: Blog, isBackupMode: Bool = false) {
self.blog = blog
self.isBackupMode = isBackupMode
self.backupTracker = isBackupMode ? DownloadableBackupTracker(blog: blog) : nil
}

func onAppear() {
backupTracker?.startTracking()

guard response == nil else { return }
onRefreshNeeded()
}
Expand All @@ -48,6 +52,9 @@ final class ActivityLogsViewModel: ObservableObject {
func refresh() async {
isLoading = true
error = nil

backupTracker?.refreshBackupStatus()

Task {
do {
let response = try await makeResponse(searchText: searchText, parameters: parameters)
Expand All @@ -69,6 +76,10 @@ final class ActivityLogsViewModel: ObservableObject {
try await makeResponse(searchText: searchText, parameters: parameters)
}

func onDisappear() {
backupTracker?.stopTracking()
}

func fetchActivityGroups(after: Date? = nil, before: Date? = nil) async throws -> [WordPressKit.ActivityGroup] {
guard let siteID = blog.dotComID?.intValue,
let api = blog.wordPressComRestApi else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import SwiftUI
import WordPressUI
import WordPressKit

/// Represents the current status of a downloadable backup.
enum DownloadableBackupStatus {
/// Backup creation is in progress or processing
case inProgress(backup: JetpackBackup, progress: Int)

/// Backup is ready for download
case readyToDownload(backup: JetpackBackup, url: URL, validUntil: Date)

init?(backup: JetpackBackup?) {
guard let backup else {
return nil
}

// Determine the status based on the backup properties
if let urlString = backup.url,
let url = URL(string: urlString),
let validUntil = backup.validUntil,
Date() < validUntil {
// Download is ready and valid
self = .readyToDownload(backup: backup, url: url, validUntil: validUntil)
} else if let progress = backup.progress, progress > 0 {
// Backup is being created or processing
self = .inProgress(backup: backup, progress: progress)
} else {
// Backup exists but in an unknown state
return nil
}
}
}

struct DownloadableBackupSection: View {
@ObservedObject var backupTracker: DownloadableBackupTracker

var body: some View {
if let status = DownloadableBackupStatus(backup: backupTracker.backup) {
CardView {
switch status {
case .inProgress(let backup, let progress):
BackupInProgressView(backup: backup, progress: progress)

case .readyToDownload(let backup, let url, let validUntil):
BackupDownloadHeaderView(
backup: backup,
url: url,
validUntil: validUntil,
backupTracker: backupTracker
)
}
}
.padding(.horizontal)
.padding(.top, 8)
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
}
}

// MARK: - Private Views

private struct BackupInProgressView: View {
let backup: JetpackBackup
let progress: Int

private var progressFloat: Float {
max(Float(progress) / 100, 0.05) // Show at least 5% for UX
}

var body: some View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(Strings.InProgress.title)
.font(.subheadline)
.fontWeight(.medium)
.foregroundStyle(.primary)

Text(Strings.InProgress.message)
.font(.footnote)
.foregroundStyle(.secondary)
}

HStack {
ProgressView(value: progressFloat)
.progressViewStyle(.linear)
.tint(.accentColor)

Text("\(progress)%")
.font(.caption)
.foregroundStyle(.secondary)
.monospacedDigit()
}
}
}
}

private struct BackupDownloadHeaderView: View {
let backup: JetpackBackup
let url: URL
let validUntil: Date
let backupTracker: DownloadableBackupTracker

private var formattedBackupDate: String {
backup.backupPoint.formatted(date: .abbreviated, time: .shortened)
}

private var formattedExpiryDate: String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
return formatter.localizedString(for: validUntil, relativeTo: Date())
}

var body: some View {
VStack(alignment: .leading, spacing: 12) {
headerView
downloadButton
}
}

private var headerView: some View {
HStack(alignment: .top) {
VStack(alignment: .leading, spacing: 4) {
Text(Strings.Download.successTitle)
.font(.subheadline)
.fontWeight(.medium)
.foregroundStyle(.primary)

Text(String(format: Strings.Download.message, formattedBackupDate))
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)

Text(String(format: Strings.Download.expiresIn, formattedExpiryDate))
.font(.caption)
.foregroundStyle(.tertiary)
}

Spacer()

Button(action: {
withAnimation {
backupTracker.dismissBackupNotice()
}
}) {
Image(systemName: "xmark")
.font(.caption)
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
}
}

private var downloadButton: some View {
HStack(spacing: 12) {
Button(action: {
WPAnalytics.track(.backupFileDownloadTapped)
UIApplication.shared.open(url)
}) {
HStack(spacing: 4) {
Image(systemName: "arrow.down.circle")
Text(Strings.Download.download)
}
.fontWeight(.medium)
}
.buttonStyle(.borderedProminent)

Spacer()
}
}
}

// MARK: - Strings

private enum Strings {
enum InProgress {
static let title = NSLocalizedString(
"backup.inProgress.title",
value: "Creating downloadable backup",
comment: "Title shown when a downloadable backup is being created"
)

static let message = NSLocalizedString(
"backup.inProgress.message",
value: "Preparing your site backup for download",
comment: "Message shown when a downloadable backup is in progress"
)
}

enum Download {
static let successTitle = NSLocalizedString(
"backup.download.header.title",
value: "Backup ready to download",
comment: "Title shown when a backup is ready to download"
)

static let message = NSLocalizedString(
"backup.download.header.message",
value: "Your backup from %@ is ready",
comment: "Message displayed when a backup has finished. %@ is the date and time."
)

static let expiresIn = NSLocalizedString(
"backup.download.header.expiresIn",
value: "Expires %@",
comment: "Shows when the download link will expire. %@ is the relative time (e.g., 'in 2 hours')"
)

static let download = NSLocalizedString(
"backup.download.header.download",
value: "Download",
comment: "Download button title"
)
}

}
Loading