mirror of
https://github.com/signalapp/Signal-iOS.git
synced 2025-12-05 01:10:41 +00:00
Add indeterminate progress to Link'n'Sync provisioning
This commit is contained in:
@@ -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 */,
|
||||
|
||||
1
Signal/Lottie/linear_indeterminate.json
Normal file
1
Signal/Lottie/linear_indeterminate.json
Normal 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":[]}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user