From b94e9bba03dd0882daf374e33d3ba0da6045b10a Mon Sep 17 00:00:00 2001 From: Shonchik Date: Tue, 21 Jul 2026 16:06:38 +0700 Subject: [PATCH 1/5] Fix gif attachment --- .../ChatExample/Screens/ChatExampleView.swift | 1 + .../ExyteChat/Utils/CachedAnimatedImage.swift | 37 +++++++++++++ .../Views/Attachments/AttachmentCell.swift | 53 ++++++++++++++----- .../Views/Attachments/AttachmentsPage.swift | 30 +++++++---- 4 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 Sources/ExyteChat/Utils/CachedAnimatedImage.swift diff --git a/ChatExample/ChatExample/Screens/ChatExampleView.swift b/ChatExample/ChatExample/Screens/ChatExampleView.swift index 72a0c1db..dd3191b3 100644 --- a/ChatExample/ChatExample/Screens/ChatExampleView.swift +++ b/ChatExample/ChatExample/Screens/ChatExampleView.swift @@ -94,6 +94,7 @@ struct ChatExampleView: View { .setMediaPickerLiveCameraStyle(.prominant) .setRecorderSettings(recorderSettings) .messageReactionDelegate(viewModel) + .setAvailableInputs([.text, .media, .giphy, .audio]) .swipeActions(edge: .leading, performsFirstActionWithFullSwipe: true, items: [replyAction]) .navigationBarBackButtonHidden() .navigationBarTitleDisplayMode(.inline) diff --git a/Sources/ExyteChat/Utils/CachedAnimatedImage.swift b/Sources/ExyteChat/Utils/CachedAnimatedImage.swift new file mode 100644 index 00000000..859eee87 --- /dev/null +++ b/Sources/ExyteChat/Utils/CachedAnimatedImage.swift @@ -0,0 +1,37 @@ +// +// CachedAnimatedImage.swift +// + +import SwiftUI +import Kingfisher + +struct CachedAnimatedImage: View { + + let url: URL + let cacheKey: String? + let contentMode: SwiftUI.ContentMode + @ViewBuilder var placeholder: () -> Placeholder + + var body: some View { + // `.network(...)` only works for http(s) URLs; local `file://` URLs (e.g. GIFs picked + // from the photo library) need `.provider(LocalFileImageDataProvider)`, which + // `convertToSource` picks automatically based on the URL scheme. + KFAnimatedImage(source: url.convertToSource(overrideCacheKey: cacheKey)) + .configure { view in +#if canImport(UIKit) + view.contentMode = contentMode == .fill ? .scaleAspectFill : .scaleAspectFit + view.clipsToBounds = true +#elseif canImport(AppKit) + view.imageScaling = .scaleProportionallyUpOrDown +#endif + } + .cacheOriginalImage() + .placeholder(placeholder) + } +} + +extension URL { + var isGIF: Bool { + pathExtension.lowercased() == "gif" + } +} diff --git a/Sources/ExyteChat/Views/Attachments/AttachmentCell.swift b/Sources/ExyteChat/Views/Attachments/AttachmentCell.swift index 6ded8479..2f2556f3 100644 --- a/Sources/ExyteChat/Views/Attachments/AttachmentCell.swift +++ b/Sources/ExyteChat/Views/Attachments/AttachmentCell.swift @@ -184,23 +184,48 @@ struct AsyncImageView: View { let attachment: Attachment let size: CGSize + private var animatedURL: (url: URL, cacheKey: String?)? { + if attachment.thumbnail.isGIF { + return (attachment.thumbnail, attachment.thumbnailCacheKey) + } else if attachment.full.isGIF { + return (attachment.full, attachment.fullCacheKey) + } + return nil + } + var body: some View { - CachedAsyncImage( - url: attachment.thumbnail, - cacheKey: attachment.thumbnailCacheKey - ) { imageView in - imageView - .resizable() - .scaledToFill() - .frame(width: size.width, height: size.height) - .clipped() - } placeholder: { - ZStack { - Rectangle() - .foregroundColor(theme.colors.inputBG) + if let animatedURL { + CachedAnimatedImage( + url: animatedURL.url, + cacheKey: animatedURL.cacheKey, + contentMode: .fill + ) { + placeholder + } + .frame(width: size.width, height: size.height) + .clipped() + } else { + CachedAsyncImage( + url: attachment.thumbnail, + cacheKey: attachment.thumbnailCacheKey + ) { imageView in + imageView + .resizable() + .scaledToFill() .frame(width: size.width, height: size.height) - ActivityIndicator(size: 30, showBackground: false) + .clipped() + } placeholder: { + placeholder } } } + + private var placeholder: some View { + ZStack { + Rectangle() + .foregroundColor(theme.colors.inputBG) + .frame(width: size.width, height: size.height) + ActivityIndicator(size: 30, showBackground: false) + } + } } diff --git a/Sources/ExyteChat/Views/Attachments/AttachmentsPage.swift b/Sources/ExyteChat/Views/Attachments/AttachmentsPage.swift index 02646d8e..2eb71194 100644 --- a/Sources/ExyteChat/Views/Attachments/AttachmentsPage.swift +++ b/Sources/ExyteChat/Views/Attachments/AttachmentsPage.swift @@ -14,18 +14,28 @@ struct AttachmentsPage: View { var body: some View { if attachment.type == .image { ZoomableContainer { - CachedAsyncImage( - url: attachment.full, - cacheKey: attachment.fullCacheKey - ) { phase in - switch phase { - case let .success(image): - image - .resizable() - .aspectRatio(contentMode: .fit) - default: + if attachment.full.isGIF { + CachedAnimatedImage( + url: attachment.full, + cacheKey: attachment.fullCacheKey, + contentMode: .fit + ) { ActivityIndicator() } + } else { + CachedAsyncImage( + url: attachment.full, + cacheKey: attachment.fullCacheKey + ) { phase in + switch phase { + case let .success(image): + image + .resizable() + .aspectRatio(contentMode: .fit) + default: + ActivityIndicator() + } + } } } } else if attachment.type == .video { From 6acdcfc20c9e065fb5203108fe5b8be78d92b7ad Mon Sep 17 00:00:00 2001 From: Shonchik Date: Tue, 21 Jul 2026 16:43:59 +0700 Subject: [PATCH 2/5] Add ability to choose inputViewRightButton --- README.md | 4 +++ .../Views/ChatCustomizationParameters.swift | 1 + Sources/ExyteChat/Views/ChatView.swift | 1 + .../ExyteChat/Views/InputView/InputView.swift | 32 +++++++++++++++++-- Sources/ExyteChat/Views/PublicAPI.swift | 7 ++++ 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6da72ae1..c50a493f 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,10 @@ ChatView(messages: viewModel.messages) { draft in `photoPickerBackend` - choose which photo/video picker is presented when the user taps to attach media: - `.custom` (default) - ExyteMediaPicker fully customizable built-in media picker - `.system` - Apple's native `PhotosPicker` for apps that don't need a customized picker UI. Selected items are shown as a removable thumbnail strip above the input field, and camera capture always uses the ExyteMediaPicker regardless of this setting. +`inputViewRightButtonType` - choose which action occupies the input field's dedicated right-side button slot when the composer is empty: + - `.camera` (default) - camera button on the right, giphy button stays on the left next to attach (shown only if that action is actually available via `setAvailableInputs`) + - `.giphy` - giphy button on the right, left side shows only attach + - `.none` - no button on the right side ### Customize default colors and images You can use `chatTheme` to customize colors and images of default UI. You can pass all/some colors and images: diff --git a/Sources/ExyteChat/Views/ChatCustomizationParameters.swift b/Sources/ExyteChat/Views/ChatCustomizationParameters.swift index 79c11e48..ac5ac529 100644 --- a/Sources/ExyteChat/Views/ChatCustomizationParameters.swift +++ b/Sources/ExyteChat/Views/ChatCustomizationParameters.swift @@ -79,6 +79,7 @@ struct InputViewCustomizationParameters { var audioRecordingMode: AudioRecordingMode = .holdToRecord var mediaPickerParameters = MediaPickerParameters() var photoPickerBackend: PhotoPickerBackend = .custom + var inputViewRightButtonType: InputViewRightButtonType = .camera } public typealias MediaPickerParameters = ExyteMediaPicker.MediaPickerCutomizationParameters diff --git a/Sources/ExyteChat/Views/ChatView.swift b/Sources/ExyteChat/Views/ChatView.swift index ae887793..8c878fd4 100644 --- a/Sources/ExyteChat/Views/ChatView.swift +++ b/Sources/ExyteChat/Views/ChatView.swift @@ -394,6 +394,7 @@ public struct ChatView Bool { return availableInputs.contains(AvailableInputType.media) } + + private func effectiveRightButtonType() -> InputViewRightButtonType { + + switch inputViewRightButtonType { + case .giphy: + return isGiphyAvailable() ? .giphy : .none + case .camera: + return isMediaAvailable() ? .camera : .none + case .none: + return .none + } + } } private struct MediaAttachmentThumbnail: View { diff --git a/Sources/ExyteChat/Views/PublicAPI.swift b/Sources/ExyteChat/Views/PublicAPI.swift index 9a374015..05b84f68 100644 --- a/Sources/ExyteChat/Views/PublicAPI.swift +++ b/Sources/ExyteChat/Views/PublicAPI.swift @@ -382,4 +382,11 @@ public extension ChatView { view.inputViewCustomizationParameters.photoPickerBackend = backend return view } + + /// Which action occupies InputView's dedicated right-side button slot: camera, giphy attachment, or none. + func inputViewRightButtonType(_ type: InputViewRightButtonType) -> ChatView { + var view = self + view.inputViewCustomizationParameters.inputViewRightButtonType = type + return view + } } From 1fe82c53fb93b197c513d5fe5a67ea4440de2f5c Mon Sep 17 00:00:00 2001 From: Shonchik Date: Tue, 21 Jul 2026 16:52:30 +0700 Subject: [PATCH 3/5] Fix lockRecordButton --- .../ExyteChat/Views/InputView/InputView.swift | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Sources/ExyteChat/Views/InputView/InputView.swift b/Sources/ExyteChat/Views/InputView/InputView.swift index b95703af..c9a4e25b 100644 --- a/Sources/ExyteChat/Views/InputView/InputView.swift +++ b/Sources/ExyteChat/Views/InputView/InputView.swift @@ -109,8 +109,9 @@ struct InputView: View { private var state: InputViewState { viewModel.state } - - @State private var overlaySize: CGSize = .zero + + @State private var stopRecordButtonSize: CGSize = .zero + @State private var lockRecordButtonSize: CGSize = .zero @State private var recordButtonFrame: CGRect = .zero @State private var lockRecordFrame: CGRect = .zero @@ -283,15 +284,15 @@ struct InputView: View { } .compositingGroup() .overlay(alignment: .top) { - Group { - if state == .isRecordingTap { - stopRecordButton - } else if state == .isRecordingHold { - lockRecordButton - } + if state == .isRecordingTap { + stopRecordButton + .sizeGetter($stopRecordButtonSize) + .offset(y: -stopRecordButtonSize.height - stopRecordButtonOffset) + } else if state == .isRecordingHold { + lockRecordButton + .sizeGetter($lockRecordButtonSize) + .offset(y: -lockRecordButtonSize.height - stopRecordButtonOffset) } - .sizeGetter($overlaySize) - .offset(y: -overlaySize.height - stopRecordButtonOffset) } } .viewSize(48) From 62e520b88860b76261ce31459b0cf1217a47c3e0 Mon Sep 17 00:00:00 2001 From: Shonchik Date: Fri, 24 Jul 2026 15:36:31 +0700 Subject: [PATCH 4/5] Change attachment button --- Package.swift | 7 +- README.md | 6 +- .../ExyteChat/Extensions/URL+Extensions.swift | 14 ++ .../ExyteChat/Model/ChatLocalization.swift | 13 +- Sources/ExyteChat/Theme/ChatTheme.swift | 5 +- .../ExyteChat/Utils/CachedAnimatedImage.swift | 6 - .../Views/ChatCustomizationParameters.swift | 1 - Sources/ExyteChat/Views/ChatView.swift | 1 - .../ExyteChat/Views/InputView/InputView.swift | 180 +++++++++++++----- Sources/ExyteChat/Views/PublicAPI.swift | 7 - 10 files changed, 165 insertions(+), 75 deletions(-) create mode 100644 Sources/ExyteChat/Extensions/URL+Extensions.swift diff --git a/Package.swift b/Package.swift index b53a9238..1f9d10dd 100644 --- a/Package.swift +++ b/Package.swift @@ -30,6 +30,10 @@ let package = Package( url: "https://github.com/onevcat/Kingfisher", from: "8.5.0" ), + .package( + url: "https://github.com/exyte/AnchoredPopup.git", + from: "1.1.3" + ), ], targets: [ .target( @@ -38,7 +42,8 @@ let package = Package( .product(name: "ExyteMediaPicker", package: "MediaPicker"), .product(name: "ActivityIndicatorView", package: "ActivityIndicatorView"), .product(name: "GiphyUISDK", package: "giphy-ios-sdk"), - .product(name: "Kingfisher", package: "Kingfisher") + .product(name: "Kingfisher", package: "Kingfisher"), + .product(name: "AnchoredPopup", package: "AnchoredPopup") ], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") diff --git a/README.md b/README.md index c50a493f..bf906cd3 100644 --- a/README.md +++ b/README.md @@ -362,10 +362,8 @@ ChatView(messages: viewModel.messages) { draft in `photoPickerBackend` - choose which photo/video picker is presented when the user taps to attach media: - `.custom` (default) - ExyteMediaPicker fully customizable built-in media picker - `.system` - Apple's native `PhotosPicker` for apps that don't need a customized picker UI. Selected items are shown as a removable thumbnail strip above the input field, and camera capture always uses the ExyteMediaPicker regardless of this setting. -`inputViewRightButtonType` - choose which action occupies the input field's dedicated right-side button slot when the composer is empty: - - `.camera` (default) - camera button on the right, giphy button stays on the left next to attach (shown only if that action is actually available via `setAvailableInputs`) - - `.giphy` - giphy button on the right, left side shows only attach - - `.none` - no button on the right side + +The attach button on the left opens a popup menu to choose between Media and GIF when both are available via `setAvailableInputs`. If `photoPickerBackend` is `.system`, a separate Camera entry is also added to the menu, since Apple's native `PhotosPicker` can't capture photos/video itself; with the default `.custom` backend, camera capture is reachable from within the media picker itself, so no separate entry is needed. If there's only one attachment option in total, tapping the button triggers it directly with no popup. The right side of the input field shows a clear ("x") button to quickly clear typed text once the field is non-empty. ### Customize default colors and images You can use `chatTheme` to customize colors and images of default UI. You can pass all/some colors and images: diff --git a/Sources/ExyteChat/Extensions/URL+Extensions.swift b/Sources/ExyteChat/Extensions/URL+Extensions.swift new file mode 100644 index 00000000..9f520148 --- /dev/null +++ b/Sources/ExyteChat/Extensions/URL+Extensions.swift @@ -0,0 +1,14 @@ +// +// URL+Extensions.swift +// Chat +// +// Created by Exyte on 23.07.2026. +// + +import Foundation + +extension URL { + var isGIF: Bool { + pathExtension.lowercased() == "gif" + } +} diff --git a/Sources/ExyteChat/Model/ChatLocalization.swift b/Sources/ExyteChat/Model/ChatLocalization.swift index 3d4fb28f..bd9e4850 100644 --- a/Sources/ExyteChat/Model/ChatLocalization.swift +++ b/Sources/ExyteChat/Model/ChatLocalization.swift @@ -15,8 +15,11 @@ public struct ChatLocalization: Hashable { public var waitingForNetwork: String public var recordingText: String public var replyToText: String + public var attachMediaText: String + public var attachGifText: String + public var attachCameraText: String - public init(inputPlaceholder: String, signatureText: String, cancelButtonText: String, recentToggleText: String, waitingForNetwork: String, recordingText: String, replyToText: String) { + public init(inputPlaceholder: String, signatureText: String, cancelButtonText: String, recentToggleText: String, waitingForNetwork: String, recordingText: String, replyToText: String, attachMediaText: String = String(localized: "Media"), attachGifText: String = String(localized: "GIF"), attachCameraText: String = String(localized: "Camera")) { self.inputPlaceholder = inputPlaceholder self.signatureText = signatureText self.cancelButtonText = cancelButtonText @@ -24,6 +27,9 @@ public struct ChatLocalization: Hashable { self.waitingForNetwork = waitingForNetwork self.recordingText = recordingText self.replyToText = replyToText + self.attachMediaText = attachMediaText + self.attachGifText = attachGifText + self.attachCameraText = attachCameraText } public static var defaultLocalization: ChatLocalization { @@ -34,7 +40,10 @@ public struct ChatLocalization: Hashable { recentToggleText: String(localized: "Recents"), waitingForNetwork: String(localized: "Waiting for network"), recordingText: String(localized: "Recording..."), - replyToText: String(localized: "Reply to") + replyToText: String(localized: "Reply to"), + attachMediaText: String(localized: "Media"), + attachGifText: String(localized: "GIF"), + attachCameraText: String(localized: "Camera") ) } } diff --git a/Sources/ExyteChat/Theme/ChatTheme.swift b/Sources/ExyteChat/Theme/ChatTheme.swift index d96802ad..a405f87d 100644 --- a/Sources/ExyteChat/Theme/ChatTheme.swift +++ b/Sources/ExyteChat/Theme/ChatTheme.swift @@ -275,6 +275,7 @@ public struct ChatTheme: Sendable { public var attach: Image public var attachCamera: Image public var microphone: Image + public var clearText: Image } public struct FullscreenMedia: Sendable { @@ -359,6 +360,7 @@ public struct ChatTheme: Sendable { attach: Image? = nil, attachCamera: Image? = nil, microphone: Image? = nil, + clearText: Image? = nil, fullscreenPlay: Image? = nil, fullscreenPause: Image? = nil, fullscreenMute: Image? = nil, @@ -420,7 +422,8 @@ public struct ChatTheme: Sendable { sticker: sticker ?? Image("sticker", bundle: .current), attach: attach ?? Image("attach", bundle: .current), attachCamera: attachCamera ?? Image("attachCamera", bundle: .current), - microphone: microphone ?? Image("microphone", bundle: .current) + microphone: microphone ?? Image("microphone", bundle: .current), + clearText: clearText ?? Image(systemName: "xmark.circle.fill") ) self.fullscreenMedia = FullscreenMedia( diff --git a/Sources/ExyteChat/Utils/CachedAnimatedImage.swift b/Sources/ExyteChat/Utils/CachedAnimatedImage.swift index 859eee87..6a1809ad 100644 --- a/Sources/ExyteChat/Utils/CachedAnimatedImage.swift +++ b/Sources/ExyteChat/Utils/CachedAnimatedImage.swift @@ -29,9 +29,3 @@ struct CachedAnimatedImage: View { .placeholder(placeholder) } } - -extension URL { - var isGIF: Bool { - pathExtension.lowercased() == "gif" - } -} diff --git a/Sources/ExyteChat/Views/ChatCustomizationParameters.swift b/Sources/ExyteChat/Views/ChatCustomizationParameters.swift index ac5ac529..79c11e48 100644 --- a/Sources/ExyteChat/Views/ChatCustomizationParameters.swift +++ b/Sources/ExyteChat/Views/ChatCustomizationParameters.swift @@ -79,7 +79,6 @@ struct InputViewCustomizationParameters { var audioRecordingMode: AudioRecordingMode = .holdToRecord var mediaPickerParameters = MediaPickerParameters() var photoPickerBackend: PhotoPickerBackend = .custom - var inputViewRightButtonType: InputViewRightButtonType = .camera } public typealias MediaPickerParameters = ExyteMediaPicker.MediaPickerCutomizationParameters diff --git a/Sources/ExyteChat/Views/ChatView.swift b/Sources/ExyteChat/Views/ChatView.swift index 8c878fd4..ae887793 100644 --- a/Sources/ExyteChat/Views/ChatView.swift +++ b/Sources/ExyteChat/Views/ChatView.swift @@ -394,7 +394,6 @@ public struct ChatView 1 { theme.images.inputView.attach .viewSize(24) .padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 6)) + .useAsPopupAnchor(id: attachMenuPopupId, contentBuilder: { + attachMenuContent(items) + }, customize: { + $0.position(.absolute(.bottomLeading, position: CGPoint(x: attachMenuLeftMargin, y: inputBarFrame.minY - attachMenuGap))) + .background(.none) + .closeOnTapOutside(true) + .animation(.default) + }) + } else if let item = items.first, item.action == .photo { + Button { + onAction(.photo) + } label: { + theme.images.inputView.attach + .viewSize(24) + .padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 6)) + } + } else if let item = items.first, item.action == .giphy { + Button { + onAction(.giphy) + } label: { + theme.images.inputView.sticker + .resizable() + .viewSize(24) + .padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 6)) + } } } - - var giphyButton: some View { + + private var attachMenuPopupId: String { + "exyte-chat-attach-menu-\(inputFieldId)" + } + + private func attachMenuContent(_ items: [AttachMenuItem]) -> some View { + HStack { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(items.enumerated()), id: \.offset) { index, item in + if index > 0 { + Divider() + } + AttachMenuRow(icon: item.icon, title: item.title) { + onAction(item.action) + } + } + } + .padding(.vertical, 4) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(theme.colors.inputBG) + .shadow(color: .black.opacity(0.15), radius: 8, y: 2) + ) + + Spacer() + .frame(maxWidth: .infinity) + } + } + + var clearTextButton: some View { Button { - onAction(.giphy) + viewModel.text = "" } label: { - theme.images.inputView.sticker + theme.images.inputView.clearText .resizable() - .viewSize(24) - .padding(EdgeInsets(top: 12, leading: 6, bottom: 12, trailing: 12)) + .renderingMode(.template) + .foregroundColor(theme.colors.mainText.opacity(0.6)) + .viewSize(18) + .padding(EdgeInsets(top: 12, leading: 8, bottom: 12, trailing: 12)) } } - + var addButton: some View { Button { onAction(.add) @@ -425,16 +491,6 @@ struct InputView: View { } } - var cameraButton: some View { - Button { - onAction(.camera) - } label: { - theme.images.inputView.attachCamera - .viewSize(24) - .padding(EdgeInsets(top: 12, leading: 8, bottom: 12, trailing: 12)) - } - } - var sendButton: some View { Button { onAction(.send) @@ -661,17 +717,37 @@ struct InputView: View { private func isMediaAvailable() -> Bool { return availableInputs.contains(AvailableInputType.media) } +} + +private struct AttachMenuRow: View { + @Environment(\.chatTheme) private var theme + @Environment(\.anchoredPopupDismiss) private var dismissPopup - private func effectiveRightButtonType() -> InputViewRightButtonType { + let icon: Image + let title: String + let action: () -> Void - switch inputViewRightButtonType { - case .giphy: - return isGiphyAvailable() ? .giphy : .none - case .camera: - return isMediaAvailable() ? .camera : .none - case .none: - return .none + var body: some View { + Button { + action() + dismissPopup?() + } label: { + HStack(spacing: 10) { + icon + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(theme.colors.mainTint) + Text(title) + .font(.callout) + .foregroundColor(theme.colors.mainText) + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + .contentShape(Rectangle()) } + .buttonStyle(.plain) } } diff --git a/Sources/ExyteChat/Views/PublicAPI.swift b/Sources/ExyteChat/Views/PublicAPI.swift index 05b84f68..9a374015 100644 --- a/Sources/ExyteChat/Views/PublicAPI.swift +++ b/Sources/ExyteChat/Views/PublicAPI.swift @@ -382,11 +382,4 @@ public extension ChatView { view.inputViewCustomizationParameters.photoPickerBackend = backend return view } - - /// Which action occupies InputView's dedicated right-side button slot: camera, giphy attachment, or none. - func inputViewRightButtonType(_ type: InputViewRightButtonType) -> ChatView { - var view = self - view.inputViewCustomizationParameters.inputViewRightButtonType = type - return view - } } From 06d64bb6b74c37839b7082800207393312ae33a2 Mon Sep 17 00:00:00 2001 From: Shonchik Date: Sun, 26 Jul 2026 10:58:50 +0700 Subject: [PATCH 5/5] Fix --- .../ExyteChat/Views/InputView/InputView.swift | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/Sources/ExyteChat/Views/InputView/InputView.swift b/Sources/ExyteChat/Views/InputView/InputView.swift index 7fc81634..c7b60d68 100644 --- a/Sources/ExyteChat/Views/InputView/InputView.swift +++ b/Sources/ExyteChat/Views/InputView/InputView.swift @@ -120,6 +120,12 @@ struct InputView: View { private let attachMenuLeftMargin: CGFloat = 14 private let attachMenuGap: CGFloat = 16 + private struct AttachMenuItem { + let icon: Image + let title: String + let action: InputViewAction + } + var body: some View { VStack { viewOnTop @@ -199,12 +205,12 @@ struct InputView: View { Group { switch state { case .empty, .waitingForRecordingPermission: - Color.clear.frame(width: 8, height: 1) + Color.clear.frame(width: 1, height: 1) case .hasTextOrMedia: if case .message = style, !viewModel.text.isEmpty { clearTextButton } else { - Color.clear.frame(width: 8, height: 1) + Color.clear.frame(width: 1, height: 1) } case .isRecordingHold, .isRecordingTap: recordDurationInProcess @@ -213,7 +219,7 @@ struct InputView: View { case .playingRecording, .pausedRecording: recordDurationLeft default: - Color.clear.frame(width: 8, height: 1) + Color.clear.frame(width: 1, height: 1) } } .frame(minHeight: 48) @@ -383,12 +389,6 @@ struct InputView: View { } } - private struct AttachMenuItem { - let icon: Image - let title: String - let action: InputViewAction - } - private var attachMenuItems: [AttachMenuItem] { var items: [AttachMenuItem] = [] if isMediaAvailable() { @@ -411,14 +411,14 @@ struct InputView: View { theme.images.inputView.attach .viewSize(24) .padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 6)) - .useAsPopupAnchor(id: attachMenuPopupId, contentBuilder: { + .useAsPopupAnchor(id: attachMenuPopupId) { attachMenuContent(items) - }, customize: { + } customize: { $0.position(.absolute(.bottomLeading, position: CGPoint(x: attachMenuLeftMargin, y: inputBarFrame.minY - attachMenuGap))) .background(.none) .closeOnTapOutside(true) .animation(.default) - }) + } } else if let item = items.first, item.action == .photo { Button { onAction(.photo) @@ -444,27 +444,19 @@ struct InputView: View { } private func attachMenuContent(_ items: [AttachMenuItem]) -> some View { - HStack { - VStack(alignment: .leading, spacing: 0) { - ForEach(Array(items.enumerated()), id: \.offset) { index, item in - if index > 0 { - Divider() - } - AttachMenuRow(icon: item.icon, title: item.title) { - onAction(item.action) - } + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(items.enumerated()), id: \.offset) { index, item in + AttachMenuRow(icon: item.icon, title: item.title) { + onAction(item.action) } } - .padding(.vertical, 4) - .background( - RoundedRectangle(cornerRadius: 14) - .fill(theme.colors.inputBG) - .shadow(color: .black.opacity(0.15), radius: 8, y: 2) - ) - - Spacer() - .frame(maxWidth: .infinity) } + .padding(.vertical, 4) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(theme.colors.inputBG) + .shadow(color: .black.opacity(0.15), radius: 8, y: 2) + ) } var clearTextButton: some View {