Add indeterminate progress to Link'n'Sync provisioning

This commit is contained in:
Elaine
2025-01-15 16:01:03 -07:00
committed by GitHub
parent 63c2582a22
commit d675fae6fc
5 changed files with 148 additions and 33 deletions

View File

@@ -1608,6 +1608,7 @@
B93296672BB5CF7500B8BD39 /* NicknameRecordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93296662BB5CF7500B8BD39 /* NicknameRecordStore.swift */; };
B93296692BBB3FF200B8BD39 /* ProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95BBAC12BB36025009EFB4A /* ProfileName.swift */; };
B9488E752CDED27200C1294B /* ScrollOffset.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9488E742CDED27200C1294B /* ScrollOffset.swift */; };
B94E48012D385D7800128318 /* linear_indeterminate.json in Resources */ = {isa = PBXBuildFile; fileRef = B94E48002D385D7800128318 /* linear_indeterminate.json */; };
B95A765C2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95A765B2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift */; };
B95A765E2B76E93500AA7E97 /* FindByUsernameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95A765D2B76E93500AA7E97 /* FindByUsernameViewController.swift */; };
B96D6D792B9F83270039EB99 /* SignalSymbols-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = B96D6D782B9F83270039EB99 /* SignalSymbols-Regular.otf */; };
@@ -5361,6 +5362,7 @@
B93296642BB5CF3200B8BD39 /* NicknameRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameRecord.swift; sourceTree = "<group>"; };
B93296662BB5CF7500B8BD39 /* NicknameRecordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameRecordStore.swift; sourceTree = "<group>"; };
B9488E742CDED27200C1294B /* ScrollOffset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollOffset.swift; sourceTree = "<group>"; };
B94E48002D385D7800128318 /* linear_indeterminate.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = linear_indeterminate.json; sourceTree = "<group>"; };
B95A765B2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarViewPresentationContextProvider.swift; sourceTree = "<group>"; };
B95A765D2B76E93500AA7E97 /* FindByUsernameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindByUsernameViewController.swift; sourceTree = "<group>"; };
B95BBAC12BB36025009EFB4A /* ProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileName.swift; sourceTree = "<group>"; };
@@ -10531,6 +10533,7 @@
6605D5022A86D305004DC345 /* indeterminate_spinner_white.json */,
887CD48924735D4200FDD265 /* launchApp-iPad.json */,
887CD48824735D4200FDD265 /* launchApp-iPhone.json */,
B94E48002D385D7800128318 /* linear_indeterminate.json */,
B9E322D62CD024A2006DAF3B /* linking-device-dark.json */,
B9E322D72CD024A2006DAF3B /* linking-device-light.json */,
3406D32A25DD80D600885B14 /* payments_spinner.json */,
@@ -14500,6 +14503,7 @@
45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */,
887CD48B24735D4200FDD265 /* launchApp-iPad.json in Resources */,
887CD48A24735D4200FDD265 /* launchApp-iPhone.json in Resources */,
B94E48012D385D7800128318 /* linear_indeterminate.json in Resources */,
B9E322D92CD024A2006DAF3B /* linking-device-dark.json in Resources */,
B9E322D82CD024A2006DAF3B /* linking-device-light.json in Resources */,
B6F509971AA53F760068F56A /* Localizable.strings in Resources */,

View File

@@ -0,0 +1 @@
{"v":"5.9.0","fr":60,"ip":0,"op":122,"w":360,"h":4,"nm":"Progress indicator - Indeterminate - Linear","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Progress indicator - Indeterminate - Linear","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[180,2,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-144,0],[144,0]],"c":false},"ix":2},"nm":"path_line","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[0.422]},"o":{"x":[0.4],"y":[0]},"t":42,"s":[0]},{"t":120,"s":[90]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.2],"y":[0.144]},"t":0,"s":[10]},{"t":78,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"trim_path","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.172549024224,0.419607847929,0.929411768913,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"path_stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.6,"y":0.668},"o":{"x":0.4,"y":0.443},"t":0,"s":[-68,0],"to":[60.167,0],"ti":[-60.167,0]},{"t":120,"s":[293,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"path2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-144,0],[144,0]],"c":false},"ix":2},"nm":"path_line","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[0.689]},"o":{"x":[0.4],"y":[0]},"t":78,"s":[0]},{"t":120,"s":[90]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.333],"y":[0.201]},"t":44,"s":[10]},{"t":109,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"trim_path","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.172549024224,0.419607847929,0.929411768913,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"path_stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.6,"y":0.724},"o":{"x":0.4,"y":0.207},"t":24,"s":[-393,0],"to":[77.167,0],"ti":[-77.167,0]},{"t":120,"s":[70,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"path1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":122,"st":0,"bm":0}],"markers":[]}

View File

@@ -3,29 +3,68 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import UIKit
import Lottie
import SwiftUI
import SignalUI
import SignalServiceKit
// MARK: View Model
class LinkAndSyncSecondaryProgressViewModel: ObservableObject {
@Published private(set) var progress: Float = 0
@Published private(set) var taskProgress: Float = 0
@Published private(set) var canBeCancelled: Bool = false
@Published var isIndeterminate = true
@Published var linkNSyncTask: Task<Void, Error>?
@Published var didTapCancel: Bool = false
#if DEBUG
@Published var progressSourceLabel: String?
#endif
var progress: Float {
didTapCancel ? 0 : taskProgress
}
func updateProgress(_ progress: OWSProgress) {
objectWillChange.send()
#if DEBUG
progressSourceLabel = progress.currentSourceLabel
#endif
let canBeCancelled: Bool
if let label = progress.currentSourceLabel {
canBeCancelled = label != SecondaryLinkNSyncProgressPhase.waitingForBackup.rawValue
} else {
canBeCancelled = false
}
self.progress = progress.percentComplete
guard !didTapCancel else { return }
if progress.completedUnitCount > SecondaryLinkNSyncProgressPhase.waitingForBackup.percentOfTotalProgress {
isIndeterminate = false
}
withAnimation(.smooth) {
self.taskProgress = progress.percentComplete
}
self.canBeCancelled = canBeCancelled
}
func cancel(task: Task<Void, Error>) {
task.cancel()
withAnimation(.smooth(duration: 0.2)) {
didTapCancel = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
self!.isIndeterminate = true
}
}
}
// MARK: Hosting Controller
class LinkAndSyncProvisioningProgressViewController: HostingController<LinkAndSyncProvisioningProgressView> {
fileprivate var viewModel: LinkAndSyncSecondaryProgressViewModel
@@ -45,23 +84,41 @@ class LinkAndSyncProvisioningProgressViewController: HostingController<LinkAndSy
}
}
// MARK: SwiftUI View
struct LinkAndSyncProvisioningProgressView: View {
@ObservedObject fileprivate var viewModel: LinkAndSyncSecondaryProgressViewModel
@State private var indeterminateProgressShouldShow = false
private var showIndeterminateProgress: Bool {
viewModel.isIndeterminate || indeterminateProgressShouldShow
}
private var loopMode: LottieLoopMode {
viewModel.isIndeterminate ? .loop : .playOnce
}
private var progressToShow: Float {
indeterminateProgressShouldShow ? 0 : viewModel.progress
}
private var subtitle: String {
if viewModel.progress <= 0 {
if viewModel.didTapCancel {
OWSLocalizedString(
"LINK_NEW_DEVICE_SYNC_PROGRESS_TILE_CANCELLING",
comment: "Title for a progress modal that would be indicating the sync progress while it's cancelling that sync"
)
} else if indeterminateProgressShouldShow && viewModel.progress < 0.95 {
OWSLocalizedString(
"LINKING_SYNCING_PREPARING_TO_DOWNLOAD",
comment: "Progress label when the message loading has not yet started during the device linking process"
)
} else if viewModel.progress < 1 {
} else if viewModel.progress < 0.95 {
String(
format: OWSLocalizedString(
"LINK_NEW_DEVICE_SYNC_PROGRESS_PERCENT",
comment: "On a progress modal indicating the percent complete the sync process is. Embeds {{ formatted percentage }}"
),
viewModel.progress.formatted(.percent.precision(.fractionLength(0)))
progressToShow.formatted(.percent.precision(.fractionLength(0)))
)
} else {
OWSLocalizedString(
@@ -78,38 +135,68 @@ struct LinkAndSyncProvisioningProgressView: View {
"LINKING_SYNCING_MESSAGES_TITLE",
comment: "Title shown when loading messages during linking process"
))
.font(.title2)
.font(.title2.bold())
.foregroundStyle(Color.Signal.label)
.padding(.bottom, 24)
Group {
ProgressView(value: viewModel.progress)
.frame(maxWidth: 330)
.padding(.bottom, 12)
// TODO: this should become an "indefinite" animation
// when cancelled.
.animation(.default, value: viewModel.progress)
ZStack {
LinearProgressView(progress: progressToShow)
.animation(.smooth, value: indeterminateProgressShouldShow)
if showIndeterminateProgress {
LottieView(animation: .named("linear_indeterminate"))
.playing(loopMode: loopMode)
.animationDidFinish { completed in
guard completed else { return }
indeterminateProgressShouldShow = false
}
.onAppear {
indeterminateProgressShouldShow = true
}
}
}
.padding(.bottom, 12)
.onChange(of: viewModel.isIndeterminate) { isIndeterimate in
// See LinkAndSyncProgressModal.swift
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.indeterminateProgressShouldShow = false
}
}
Group {
Text(verbatim: subtitle)
.font(.footnote.monospacedDigit())
.padding(.bottom, 22)
.animation(.none, value: subtitle)
Text(OWSLocalizedString(
"LINKING_SYNCING_TIMING_INFO",
comment: "Label below the progress bar when loading messages during linking process"
))
.font(.subheadline)
}
.foregroundStyle(Color.Signal.secondaryLabel)
Spacer()
#if DEBUG
Text("DEBUG: " + (viewModel.progressSourceLabel ?? "none") + "\n\(viewModel.taskProgress)")
.padding(.top)
.foregroundStyle(Color.Signal.quaternaryLabel)
.animation(.none, value: viewModel.progressSourceLabel)
.animation(.none, value: viewModel.taskProgress)
#endif
if viewModel.canBeCancelled, let linkNSyncTask = viewModel.linkNSyncTask {
Button(CommonStrings.cancelButton) {
viewModel.didTapCancel = true
linkNSyncTask.cancel()
}
.disabled(viewModel.didTapCancel)
Spacer()
if let linkNSyncTask = viewModel.linkNSyncTask {
Button(CommonStrings.cancelButton) {
viewModel.cancel(task: linkNSyncTask)
}
.opacity(viewModel.canBeCancelled ? 1 : 0)
.disabled(!viewModel.canBeCancelled || viewModel.didTapCancel)
.padding(.bottom, 56)
}
Group {
SignalSymbol.lock.text(dynamicTypeBaseSize: 20)
.padding(.bottom, 6)
@@ -124,13 +211,35 @@ struct LinkAndSyncProvisioningProgressView: View {
.frame(maxWidth: 412)
}
.foregroundStyle(Color.Signal.secondaryLabel)
.tint(Color.Signal.accent)
}
.tint(Color.Signal.accent)
.padding()
.multilineTextAlignment(.center)
}
// MARK: Linear Progress View
private struct LinearProgressView: View {
var progress: Float
var body: some View {
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule()
.foregroundStyle(Color.Signal.secondaryFill)
Capsule()
.foregroundStyle(Color.Signal.accent)
.frame(width: geo.size.width * CGFloat(progress))
}
}
.frame(width: 360, height: 4)
}
}
}
// MARK: Previews
#if DEBUG
@available(iOS 17, *)
#Preview {
@@ -145,18 +254,21 @@ struct LinkAndSyncProvisioningProgressView: View {
let task = Task { @MainActor in
let nonCancellableProgressSource = await progressSink.addSource(
withLabel: SecondaryLinkNSyncProgressPhase.waitingForBackup.rawValue,
unitCount: 50
unitCount: 10
)
let cancellableProgressSource = await progressSink.addSource(
withLabel: SecondaryLinkNSyncProgressPhase.downloadingBackup.rawValue,
unitCount: 90
)
let cancellableProgressSource = await progressSink.addSource(withLabel: "", unitCount: 50)
try? await Task.sleep(for: .seconds(1))
while nonCancellableProgressSource.completedUnitCount < 50 {
while nonCancellableProgressSource.completedUnitCount < 10 {
nonCancellableProgressSource.incrementCompletedUnitCount(by: 1)
try? await Task.sleep(for: .milliseconds(100))
}
while cancellableProgressSource.completedUnitCount < 50 {
while cancellableProgressSource.completedUnitCount < 90 {
cancellableProgressSource.incrementCompletedUnitCount(by: 1)
try? await Task.sleep(for: .milliseconds(100))
}

View File

@@ -65,9 +65,8 @@ class LinkAndSyncProgressViewModel: ObservableObject {
func cancel() {
linkNSyncTask?.cancel()
withAnimation(.smooth(duration: 0.2)) {
taskProgress = 0
didTapCancel = true
}
didTapCancel = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
self?.isIndeterminate = true
@@ -176,7 +175,6 @@ struct LinkAndSyncProgressView: View {
LottieView(animation: .named("circular_indeterminate"))
.playing(loopMode: loopMode)
.animationDidFinish { completed in
print("animationDidFinish: \(completed)")
guard completed else { return }
indeterminateProgressShouldShow = false
}

View File

@@ -85,11 +85,11 @@ public enum SecondaryLinkNSyncProgressPhase: String {
case downloadingBackup
case importingBackup
var percentOfTotalProgress: UInt64 {
public var percentOfTotalProgress: UInt64 {
return switch self {
case .waitingForBackup: 50
case .downloadingBackup: 30
case .importingBackup: 20
case .waitingForBackup: 5
case .downloadingBackup: 55
case .importingBackup: 40
}
}
}