diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index f962f594..f57e2176 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -129,6 +129,45 @@ struct ContentView: View { ) } + // Right edge swipe zone for easier sidebar opening (iOS-native behavior) + if !showSidebar { + HStack { + Spacer() + Color.clear + .frame(width: 20) + .contentShape(Rectangle()) + .gesture( + DragGesture(minimumDistance: 5) + .onChanged { value in + let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width + if translationWidth < 0 { + let newOffset = max(translationWidth, -300) + if abs(newOffset - sidebarDragOffset) > 2 { + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + sidebarDragOffset = newOffset + } + } + } + } + .onEnded { value in + let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width + let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width + withAnimation(.easeOut(duration: TransportConfig.uiAnimationMediumSeconds)) { + if translationWidth < -50 || velocity < -300 { + showSidebar = true + sidebarDragOffset = 0 + } else { + sidebarDragOffset = 0 + } + } + } + ) + } + .allowsHitTesting(true) + } + // Sidebar overlay HStack(spacing: 0) { // Tap to dismiss area @@ -141,31 +180,19 @@ struct ContentView: View { } } - // Only render sidebar content when it's visible or animating - if showSidebar || sidebarDragOffset != 0 { - sidebarView - #if os(macOS) - .frame(width: min(300, max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.4)) - #else - .frame(width: max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.7) - #endif - .transition(.move(edge: .trailing)) - } else { - // Empty placeholder when hidden - Color.clear - #if os(macOS) - .frame(width: min(300, max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.4)) - #else - .frame(width: max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.7) - #endif - } + // Always render sidebar to avoid layout recalculation during drag + sidebarView + #if os(macOS) + .frame(width: min(300, max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.4)) + #else + .frame(width: max(0, geometry.size.width.isNaN ? 300 : geometry.size.width) * 0.7) + #endif } .offset(x: { let dragOffset = sidebarDragOffset.isNaN ? 0 : sidebarDragOffset let width = geometry.size.width.isNaN ? 0 : max(0, geometry.size.width) - return showSidebar ? -dragOffset : width - dragOffset + return showSidebar ? dragOffset : width + dragOffset }()) - .animation(.easeInOut(duration: TransportConfig.uiAnimationSidebarSeconds), value: showSidebar) } } #if os(macOS) @@ -921,9 +948,52 @@ struct ContentView: View { .id(viewModel.allPeers.map { "\($0.peerID)-\($0.isConnected)" }.joined()) } - Spacer() - } - .background(backgroundColor) + Spacer() + } + .background(backgroundColor) + .simultaneousGesture( + DragGesture(minimumDistance: 10) + .onChanged { value in + let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width + let translationHeight = value.translation.height.isNaN ? 0 : value.translation.height + + // Only handle horizontal drags on the sidebar + guard abs(translationWidth) > abs(translationHeight) * 1.5 else { return } + + if translationWidth > 0 { + let newOffset = min(translationWidth, 300) + if abs(newOffset - sidebarDragOffset) > 2 { + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + sidebarDragOffset = newOffset + } + } + } + } + .onEnded { value in + let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width + let translationHeight = value.translation.height.isNaN ? 0 : value.translation.height + let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width + + // Only handle if drag is predominantly horizontal + guard abs(translationWidth) > abs(translationHeight) * 1.5 else { + sidebarDragOffset = 0 + return + } + + withAnimation(.easeOut(duration: TransportConfig.uiAnimationMediumSeconds)) { + if translationWidth > 100 || (translationWidth > 50 && velocity > 500) { + showSidebar = false + sidebarDragOffset = 0 + } else { + sidebarDragOffset = 0 + } + } + } + ) + + } } @@ -939,29 +1009,56 @@ struct ContentView: View { } .background(backgroundColor) .foregroundColor(textColor) - .gesture( - DragGesture() + .simultaneousGesture( + DragGesture(minimumDistance: 10) .onChanged { value in - let translation = value.translation.width.isNaN ? 0 : value.translation.width - if !showSidebar && translation < 0 { - sidebarDragOffset = max(translation, -300) - } else if showSidebar && translation > 0 { - sidebarDragOffset = min(-300 + translation, 0) + let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width + let translationHeight = value.translation.height.isNaN ? 0 : value.translation.height + + // Only handle if drag is predominantly horizontal (prevents interfering with scroll) + guard abs(translationWidth) > abs(translationHeight) * 1.5 else { return } + + if !showSidebar && translationWidth < 0 { + let newOffset = max(translationWidth, -300) + if abs(newOffset - sidebarDragOffset) > 2 { + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + sidebarDragOffset = newOffset + } + } + } else if showSidebar && translationWidth > 0 { + let newOffset = min(-300 + translationWidth, 0) + if abs(newOffset - sidebarDragOffset) > 2 { + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + sidebarDragOffset = newOffset + } + } } } .onEnded { value in - let translation = value.translation.width.isNaN ? 0 : value.translation.width + let translationWidth = value.translation.width.isNaN ? 0 : value.translation.width + let translationHeight = value.translation.height.isNaN ? 0 : value.translation.height let velocity = value.velocity.width.isNaN ? 0 : value.velocity.width + + // Only handle if drag is predominantly horizontal + guard abs(translationWidth) > abs(translationHeight) * 1.5 else { + sidebarDragOffset = 0 + return + } + withAnimation(.easeOut(duration: TransportConfig.uiAnimationMediumSeconds)) { if !showSidebar { - if translation < -100 || (translation < -50 && velocity < -500) { + if translationWidth < -100 || (translationWidth < -50 && velocity < -500) { showSidebar = true sidebarDragOffset = 0 } else { sidebarDragOffset = 0 } } else { - if translation > 100 || (translation > 50 && velocity > 500) { + if translationWidth > 100 || (translationWidth > 50 && velocity > 500) { showSidebar = false sidebarDragOffset = 0 } else {