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