better new chat animation
This commit is contained in:
@@ -15,8 +15,10 @@ struct SybilWorkspaceView: View {
|
||||
@State private var photoPickerItems: [PhotosPickerItem] = []
|
||||
@State private var isComposerDropTargeted = false
|
||||
@State private var newChatSwipeOffset: CGFloat = 0
|
||||
@State private var newChatSwipeCompletionOffset: CGFloat = 0
|
||||
@State private var newChatSwipeContainerWidth: CGFloat = NewChatSwipeMetrics.referenceWidth
|
||||
@State private var newChatSwipeIsActive = false
|
||||
@State private var newChatSwipeIsCompleting = false
|
||||
@State private var newChatSwipeHasLatched = false
|
||||
@State private var newChatSwipeDidTriggerHaptic = false
|
||||
@State private var newChatSwipeFeedbackGenerator: UIImpactFeedbackGenerator?
|
||||
@@ -55,9 +57,17 @@ struct SybilWorkspaceView: View {
|
||||
return true
|
||||
}
|
||||
|
||||
private var canRecognizeNewChatSwipe: Bool {
|
||||
canSwipeToCreateChat && !newChatSwipeIsCompleting
|
||||
}
|
||||
|
||||
private var showsNewChatSwipeBackdrop: Bool {
|
||||
canSwipeToCreateChat || newChatSwipeIsCompleting
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .trailing) {
|
||||
if canSwipeToCreateChat {
|
||||
if showsNewChatSwipeBackdrop {
|
||||
NewChatSwipeBackdrop(
|
||||
progress: NewChatSwipeMetrics.progress(for: newChatSwipeOffset, width: newChatSwipeContainerWidth),
|
||||
hasLatched: newChatSwipeHasLatched
|
||||
@@ -72,6 +82,7 @@ struct SybilWorkspaceView: View {
|
||||
.offset(x: newChatSwipeOffset)
|
||||
.blur(radius: NewChatSwipeMetrics.blurRadius(for: newChatSwipeOffset, width: newChatSwipeContainerWidth))
|
||||
}
|
||||
.offset(x: newChatSwipeCompletionOffset)
|
||||
.background(SybilTheme.background)
|
||||
.navigationTitle(viewModel.selectedTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -134,7 +145,7 @@ struct SybilWorkspaceView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background {
|
||||
NewChatSwipePanInstaller(
|
||||
isEnabled: canSwipeToCreateChat,
|
||||
isEnabled: canRecognizeNewChatSwipe,
|
||||
onBegan: { width in
|
||||
beginNewChatSwipe(containerWidth: width)
|
||||
},
|
||||
@@ -209,16 +220,34 @@ struct SybilWorkspaceView: View {
|
||||
updateNewChatSwipe(with: translationX, containerWidth: containerWidth)
|
||||
|
||||
if didFinish && newChatSwipeHasLatched {
|
||||
onRequestNewChat?()
|
||||
Task {
|
||||
await completeNewChatSwipe(containerWidth: containerWidth)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resetNewChatSwipe(animated: true)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func completeNewChatSwipe(containerWidth: CGFloat) async {
|
||||
newChatSwipeIsCompleting = true
|
||||
|
||||
withAnimation(.easeIn(duration: NewChatSwipeMetrics.completionAnimationDuration)) {
|
||||
newChatSwipeCompletionOffset = -(containerWidth + NewChatSwipeMetrics.completionOvershoot)
|
||||
}
|
||||
|
||||
try? await Task.sleep(for: .milliseconds(NewChatSwipeMetrics.completionAnimationDelayMs))
|
||||
onRequestNewChat?()
|
||||
resetNewChatSwipe(animated: false)
|
||||
}
|
||||
|
||||
private func resetNewChatSwipe(animated: Bool) {
|
||||
let reset = {
|
||||
newChatSwipeOffset = 0
|
||||
newChatSwipeCompletionOffset = 0
|
||||
newChatSwipeIsActive = false
|
||||
newChatSwipeIsCompleting = false
|
||||
newChatSwipeHasLatched = false
|
||||
newChatSwipeDidTriggerHaptic = false
|
||||
}
|
||||
@@ -443,11 +472,9 @@ struct SybilWorkspaceView: View {
|
||||
Button("Files") {
|
||||
isShowingFileImporter = true
|
||||
}
|
||||
if canPasteFromClipboard {
|
||||
Button("Paste from Clipboard") {
|
||||
Task {
|
||||
await pasteAttachmentsFromClipboard()
|
||||
}
|
||||
Button("Paste from Clipboard") {
|
||||
Task {
|
||||
await pasteAttachmentsFromClipboard()
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
@@ -500,20 +527,6 @@ struct SybilWorkspaceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var canPasteFromClipboard: Bool {
|
||||
let pasteboard = UIPasteboard.general
|
||||
if pasteboard.hasImages {
|
||||
return true
|
||||
}
|
||||
if let url = pasteboard.url, url.isFileURL {
|
||||
return true
|
||||
}
|
||||
if let string = pasteboard.string, !string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func importAttachmentsFromItemProviders(_ providers: [NSItemProvider]) async {
|
||||
do {
|
||||
@@ -558,6 +571,11 @@ struct SybilWorkspaceView: View {
|
||||
attachments.append(try SybilChatAttachmentSupport.buildTextAttachment(text: text))
|
||||
}
|
||||
|
||||
guard !attachments.isEmpty else {
|
||||
viewModel.errorMessage = "Clipboard does not contain a supported attachment."
|
||||
return
|
||||
}
|
||||
|
||||
try viewModel.appendComposerAttachments(attachments)
|
||||
composerFocused = true
|
||||
} catch {
|
||||
@@ -573,6 +591,9 @@ enum NewChatSwipeMetrics {
|
||||
static let directionDominanceRatio: CGFloat = 1.22
|
||||
static let minimumLeftwardVelocity: CGFloat = 55
|
||||
static let latchHysteresis: CGFloat = 32
|
||||
static let completionOvershoot: CGFloat = 180
|
||||
static let completionAnimationDuration = 0.24
|
||||
static let completionAnimationDelayMs: UInt64 = 240
|
||||
|
||||
static func maxTravel(for width: CGFloat) -> CGFloat {
|
||||
min(max(width * 0.46, 156), 240)
|
||||
|
||||
Reference in New Issue
Block a user