Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ These use `AnyView`, so please try to keep them easy enough
`showDateHeaders` - show section headers with dates between days, default is `true`
`isScrollEnabled` - forbid scrolling for messages' `UITableView`
`keyboardDismissMode` - set keyboard dismiss mode for the chat list (.interactive, .onDrag, or .none), default is .none
`autoFocusTextInputOnChatOpen` - automatically focus the inputTextView when the chat view is opened, default is `false`
`showMessageMenuOnLongPress` - turn menu on long tap on/off
`messageMenuAnimationDuration` - control how fast/snappy the message menu animations feel
`contentInsets` - set additional content insets for the messages list
Expand Down
121 changes: 121 additions & 0 deletions Sources/ExyteChat/Utils/ZoomableContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// ZoomableContainer.swift
// Chat
//
// Created by Thilo Molitor on 10.10.2023.
//

import SwiftUI

public struct ZoomableContainer<Content: View>: View {
let content: Content
let maxScale: CGFloat
let doubleTapScale: CGFloat
@State private var currentScale: CGFloat = 1.0
@State private var tapLocation: CGPoint = .zero

public init(maxScale:CGFloat = 4.0, doubleTapScale:CGFloat = 4.0, @ViewBuilder content: () -> Content) {
self.content = content()
self.maxScale = maxScale
self.doubleTapScale = doubleTapScale
}

public var body: some View {
ZoomableScrollView(maxScale: maxScale, scale: $currentScale, tapLocation: $tapLocation) {
content
}
.onTapGesture(count: 2, perform: { location in
tapLocation = location
currentScale = currentScale == 1.0 ? doubleTapScale : 1.0
})
}

fileprivate struct ZoomableScrollView<InnerContent: View>: UIViewRepresentable {
private var content: InnerContent
let maxScale: CGFloat
@Binding private var currentScale: CGFloat
@Binding private var tapLocation: CGPoint

init(maxScale: CGFloat, scale: Binding<CGFloat>, tapLocation: Binding<CGPoint>, @ViewBuilder content: () -> InnerContent) {
self.maxScale = maxScale
_currentScale = scale
_tapLocation = tapLocation
self.content = content()
}

func makeUIView(context: Context) -> UIScrollView {
// Setup the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = maxScale
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
scrollView.clipsToBounds = false

// Create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
scrollView.addSubview(hostedView)

return scrollView
}

func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: content), scale: $currentScale)
}

func updateUIView(_ uiView: UIScrollView, context: Context) {
// Update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = content

if uiView.zoomScale > uiView.minimumZoomScale { // Scale out
uiView.setZoomScale(currentScale, animated: true)
} else if tapLocation != .zero { // Scale in to a specific point
uiView.zoom(to: zoomRect(for: uiView, scale: uiView.maximumZoomScale, center: tapLocation), animated: true)
}

// Reset the location to prevent scaling to it in case of a negative scale (manual pinch)
// Use the main thread to prevent unexpected behavior
DispatchQueue.main.async { tapLocation = .zero }

assert(context.coordinator.hostingController.view.superview == uiView)
}

// MARK: - Utils

func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect {
let scrollViewSize = scrollView.bounds.size

let width = scrollViewSize.width / scale
let height = scrollViewSize.height / scale
let x = center.x - (width / 2.0)
let y = center.y - (height / 2.0)

return CGRect(x: x, y: y, width: width, height: height)
}

// MARK: - Coordinator

class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<InnerContent>
@Binding var currentScale: CGFloat

init(hostingController: UIHostingController<InnerContent>, scale: Binding<CGFloat>) {
self.hostingController = hostingController
_currentScale = scale
}

func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}

func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
currentScale = scale
}
}
}
}
56 changes: 0 additions & 56 deletions Sources/ExyteChat/Utils/ZoomableScrollView.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Sources/ExyteChat/Views/Attachments/AttachmentsPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct AttachmentsPage: View {

var body: some View {
if attachment.type == .image {
ZoomableScrollView {
ZoomableContainer {
CachedAsyncImage(
url: attachment.full,
cacheKey: attachment.fullCacheKey
Expand Down
1 change: 1 addition & 0 deletions Sources/ExyteChat/Views/ChatCustomizationParameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct ChatCustomizationParameters {
var showNetworkConnectionProblem: Bool = false
var showDateHeaders: Bool = true
var isScrollEnabled: Bool = true
var autoFocusTextInputOnChatOpen: Bool = false
var showMessageMenuOnLongPress: Bool = true
var keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none
var messageMenuAnimationDuration: CGFloat = 0.3
Expand Down
3 changes: 3 additions & 0 deletions Sources/ExyteChat/Views/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ public struct ChatView<MessageContent: View, InputViewContent: View, MenuAction:
viewModel.didSendMessage = didSendMessage
viewModel.inputViewModel = inputViewModel
viewModel.globalFocusState = globalFocusState
if chatCustomizationParameters.autoFocusTextInputOnChatOpen {
viewModel.focusTheInputTextView()
}
if let didUpdateAttachmentStatus {
viewModel.didUpdateAttachmentStatus = didUpdateAttachmentStatus
}
Expand Down
8 changes: 6 additions & 2 deletions Sources/ExyteChat/Views/ChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ final class ChatViewModel: ObservableObject {
}
}

func focusTheInputTextView() {
globalFocusState?.focus = .uuid(inputFieldId)
}

func messageMenuActionInternal(message: Message, action: DefaultMessageMenuAction) {
switch action {
case .copy:
Expand All @@ -61,11 +65,11 @@ final class ChatViewModel: ObservableObject {
withAnimation(.easeInOut(duration: 0.2)) {
inputViewModel?.attachments.replyMessage = message.toReplyMessage()
}
globalFocusState?.focus = .uuid(inputFieldId)
focusTheInputTextView()
case .edit(let saveClosure):
inputViewModel?.text = String(message.attributedText.characters)
inputViewModel?.edit(saveClosure)
globalFocusState?.focus = .uuid(inputFieldId)
focusTheInputTextView()
}
}
}
6 changes: 6 additions & 0 deletions Sources/ExyteChat/Views/PublicAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@
return view
}

func autoFocusTextInputOnChatOpen(_ autoFocus: Bool) -> ChatView {
var view = self
view.chatCustomizationParameters.autoFocusTextInputOnChatOpen = autoFocus
return view
}

func showMessageMenuOnLongPress(_ show: Bool) -> ChatView {
var view = self
view.chatCustomizationParameters.showMessageMenuOnLongPress = show
Expand Down Expand Up @@ -291,7 +297,7 @@
// MARK: - Built-in input view

/// binding to current text in the default input text field
public func inputViewText(_ binding: Binding<String>) -> ChatView {

Check warning on line 300 in Sources/ExyteChat/Views/PublicAPI.swift

View workflow job for this annotation

GitHub Actions / Build and Test

'public' modifier is redundant for instance method declared in a public extension
var view = self
view.inputViewCustomizationParameters.externalInputText = binding.wrappedValue
view.inputViewCustomizationParameters.onInputTextChange = { binding.wrappedValue = $0 }
Expand Down
Loading