From d54567ac8ae4edbfd77d9f3f413c098b33af641e Mon Sep 17 00:00:00 2001 From: Jaesung Lee Date: Wed, 2 Aug 2023 04:01:53 +0900 Subject: [PATCH 1/2] [ADD] Added `NextMessageField` to provide easy-customizable contents on the left/right side of the text field. --- .../MessageField/MessageField.swift | 101 +++++++++- .../MessageField/NextMessageField.swift | 184 ++++++++++++++++++ Sources/ChatUI/ChatInChannel/MessageRow.swift | 2 +- .../NextMessageField.Previews.swift | 70 +++++++ 4 files changed, 346 insertions(+), 11 deletions(-) create mode 100644 Sources/ChatUI/ChatInChannel/MessageField/NextMessageField.swift create mode 100644 Sources/ChatUI/Previews/ChatInChannel/NextMessageField.Previews.swift diff --git a/Sources/ChatUI/ChatInChannel/MessageField/MessageField.swift b/Sources/ChatUI/ChatInChannel/MessageField/MessageField.swift index 34afad7..63624d1 100644 --- a/Sources/ChatUI/ChatInChannel/MessageField/MessageField.swift +++ b/Sources/ChatUI/ChatInChannel/MessageField/MessageField.swift @@ -33,16 +33,10 @@ import PhotosUI To publish a new message, you can create a new `MessageStyle` object and send it using `send(_:)`. ```swift - let _ = Empty() - .sink( - receiveCompletion: { _ in - // Create `MessageStyle` object - let style = MessageStyle.text("{TEXT}") - // Publish the created style object via `send(_:)` - sendMessagePublisher.send(style) - }, - receiveValue: { _ in } - ) + // Create `MessageStyle` object + let style = MessageStyle.text("{TEXT}") + // Publish the created style object via `send(_:)` + sendMessagePublisher.send(style) ``` You can subscribe to `sendMessagePublisher` to handle new messages. @@ -221,6 +215,7 @@ public struct MessageField: View { isTextFieldFocused = false } + // TODO: Publishers: To customize buttons in message field and connect actions to appropriate publishers func onTapMore() { isMenuItemPresented.toggle() } @@ -257,3 +252,89 @@ public struct MessageField: View { isVoiceFieldPresented = false } } + + +// TODO: MessageField Options Extend + +/** + ```swift + struct MyAppCameraButton { + var body: some View { + Button { + MessageField.cameraTapGesturePublisher.send() + } label: { + Image.camera.medium + } + .tint(appearance.tint) + .disabled(isMenuItemPresented) // how... + .frame(width: 36, height: 36) + } + } + + MessageField(sendAction: ...) { + MessageTextField() + .fieldbar { + ItemGroup(placement: .leading) { + MyAppCameraButton() + } + + FieldItemGroup(placement: .trailing) { + VoiceButton() + + EmojiButton() + } + } + } + + + ``` + */ + +extension MessageField { + public enum Style { + case fieldOption + } + + public enum Placement { + case leading + case trailing + } + + public struct FieldOptionModifier: ViewModifier { + let placement: MessageField.Placement + let label: () -> Label + + public func body(content: Content) -> some View { + HStack(alignment: .bottom) { + if placement == .leading { + label() + } + + content + + if placement == .trailing { + label() + } + } + } + + init(_ placement: MessageField.Placement, @ViewBuilder label: @escaping () -> Label) { + self.placement = placement + self.label = label + } + } +} + +extension MessageField { + public func fieldOption(_ placement: MessageField.Placement, @ViewBuilder label: @escaping () -> Label) -> some View { + return AnyView( + modifier( + MessageField.FieldOptionModifier( + placement, + label: label + ) + ) + ) + } +} + diff --git a/Sources/ChatUI/ChatInChannel/MessageField/NextMessageField.swift b/Sources/ChatUI/ChatInChannel/MessageField/NextMessageField.swift new file mode 100644 index 0000000..6ea35e9 --- /dev/null +++ b/Sources/ChatUI/ChatInChannel/MessageField/NextMessageField.swift @@ -0,0 +1,184 @@ +// +// NextMessageField.swift +// +// +// Created by Jaesung Lee on 2023/03/28. +// + +import SwiftUI + +// TODO: Provide pre-implemented send button + +/** + The view for sending messages. + + When creating a ``NextMessageField``, you can provide an action for how to handle a new ``MessageStyle`` information in the `onSend` parameter. ``MessageStyle`` contains different types of messages, such as text, media (photo, video, document, contact) and voice. you can also provide a message-sending button by using ``sendMessagePublisher`` in the `rightLabel`. ``sendMessagePublisher`` will invoke `onSend` handler. + + ```swift + @State private var text: String = "" + + NextMessageField(text) { messageStyle in + guard !text.isEmpty else { return } + viewModel.sendMessage($0) + text = "" + } rightLabel: { + Button { + // send message by using `sendMessagePublisher`. This will invoke `onSend` handler. + sendMessagePublisher.send(.text(text)) + } label: { + // send button icon + Image.send.medium + } + .frame(width: 36, height: 36) + } + ``` + + To add some button on the left of the text field, + ```swift + NextMessageField(text) { messageStyle in + ... + } leftLabel: { + HStack { + Button(aciton: showCamera) { + Image.camera.medium + } + .frame(width: 36, height: 36) + + Button(action: showLibrary) { + Image.photoLibrary.medium + } + .frame(width: 36, height: 36) + } + ``` + + To add some button on the right of the text field, + ```swift + NextMessageField(text) { messageStyle in + ... + } rightLabel: { + HStack { + Button(aciton: { sendMessagePublisher.send(.text(text)) }) { + Image.send.medium + } + .frame(width: 36, height: 36) + } + } + ``` + + To publish a new message, you can create a new `MessageStyle` object and send it using `send(_:)`. + + ```swift + // Create `MessageStyle` object + let style = MessageStyle.text("{TEXT}") + // Publish the created style object via `send(_:)` + sendMessagePublisher.send(style) + ``` + + You can make other views to subscribe to `sendMessagePublisher` to handle new messages. + + ```swift + .onReceive(sendMessagePublisher) { messageStyle in + // Handle `messageStyle` here (e.g., sending message with the style) + } + ``` + */ +public struct NextMessageField: View { + @EnvironmentObject private var configuration: ChatConfiguration + + @Environment(\.appearance) var appearance + + @Binding public var text: String + + @FocusState private var isTextFieldFocused: Bool + @State private var textFieldHeight: CGFloat = 20 + + let leftLabel: (() -> LeftLabel)? + let rightLabel: (() -> RightLabel)? + let showsSendButtonAlways: Bool = false + let characterLimit: Int? + let onSend: (_ messageStyle: MessageStyle) -> () + + public var body: some View { + HStack(alignment: .bottom) { + if let leftLabel { + leftLabel() + .tint(appearance.tint) + } + + // TextField + HStack(alignment: .bottom) { + MessageTextField(text: $text, height: $textFieldHeight, characterLimit: characterLimit) + .frame(height: textFieldHeight < 90 ? textFieldHeight : 90) + .padding(.leading, 9) + .padding(.trailing, 4) + .focused($isTextFieldFocused) + } + .padding(6) + .background { + appearance.secondaryBackground + .clipShape(RoundedRectangle(cornerRadius: 18)) + } + + if let rightLabel { + rightLabel() + .tint(appearance.tint) + } + } + .padding(16) + .onReceive(sendMessagePublisher) { messageStyle in + onSend(messageStyle) + } + } + + public init( + _ text: Binding, + characterLimit: Int? = nil, + onSend: @escaping (_ messageStyle: MessageStyle) -> (), + @ViewBuilder leftLabel: @escaping () -> LeftLabel, + @ViewBuilder rightLabel: @escaping () -> RightLabel + ) { + self._text = text + self.characterLimit = characterLimit + self.onSend = onSend + self.leftLabel = leftLabel + self.rightLabel = rightLabel + } + + public init( + _ text: Binding, + characterLimit: Int? = nil, + onSend: @escaping (_ messageStyle: MessageStyle) -> (), + @ViewBuilder rightLabel: @escaping () -> RightLabel + ) where LeftLabel == EmptyView { + self._text = text + self.characterLimit = characterLimit + self.onSend = onSend + self.leftLabel = nil + self.rightLabel = rightLabel + } + + public init( + _ text: Binding, + characterLimit: Int? = nil, + onSend: @escaping (_ messageStyle: MessageStyle) -> (), + @ViewBuilder leftLabel: @escaping () -> LeftLabel + ) where RightLabel == EmptyView { + self._text = text + self.characterLimit = characterLimit + self.onSend = onSend + self.leftLabel = leftLabel + self.rightLabel = nil + } + + public init( + _ text: Binding, + characterLimit: Int? = nil, + onSend: @escaping (_ messageStyle: MessageStyle) -> () + ) where LeftLabel == EmptyView, RightLabel == EmptyView { + self._text = text + self.characterLimit = characterLimit + self.onSend = onSend + self.leftLabel = nil + self.rightLabel = nil + } +} diff --git a/Sources/ChatUI/ChatInChannel/MessageRow.swift b/Sources/ChatUI/ChatInChannel/MessageRow.swift index 0886da2..a617e93 100644 --- a/Sources/ChatUI/ChatInChannel/MessageRow.swift +++ b/Sources/ChatUI/ChatInChannel/MessageRow.swift @@ -61,7 +61,7 @@ public struct MessageRow: View { .padding(.horizontal, 8) } - if let reactableMessage = message as? MessageReactable { + if let reactableMessage = message as? (any MessageReactable) { switch reactableMessage.reaction { case .none: EmptyView() diff --git a/Sources/ChatUI/Previews/ChatInChannel/NextMessageField.Previews.swift b/Sources/ChatUI/Previews/ChatInChannel/NextMessageField.Previews.swift new file mode 100644 index 0000000..90cc8ee --- /dev/null +++ b/Sources/ChatUI/Previews/ChatInChannel/NextMessageField.Previews.swift @@ -0,0 +1,70 @@ +// +// NextMessageField.Previews.swift +// +// +// Created by Jaesung Lee on 2023/08/02. +// + +import SwiftUI + +struct NextMessageField_Previews: PreviewProvider { + static var previews: some View { + Preview() + .previewDisplayName("Next Message Field") + } + + struct Preview: View { + @State private var pendingMessage: Message? + @State private var text: String = "" + + var body: some View { + VStack { + if let pendingMessage = pendingMessage { + Text(pendingMessage.id) + + Text(String(describing: pendingMessage.style)) + } + + Spacer() + + NextMessageField($text) { messageStyle in + pendingMessage = Message( + id: UUID().uuidString, + sender: User.user1, + sentAt: Date().timeIntervalSince1970, + readReceipt: .sending, + style: messageStyle + ) + } leftLabel: { + HStack { + Button(action: {}) { + Image.camera.medium + } + .frame(width: 36, height: 36) + + Button(action: {}) { + Image.photoLibrary.medium + } + .frame(width: 36, height: 36) + + Button(action: {}) { + Image.mic.medium + } + .frame(width: 36, height: 36) + } + } rightLabel: { + Button { + sendMessagePublisher.send(.text(text)) + } label: { + Image.send.medium + } + .frame(width: 36, height: 36) + } + .environment(\.appearance, Appearance()) + + } + } + } + +} + From ab5b2a8b9d6e09cd6627d638ccd2154d5c64a4f6 Mon Sep 17 00:00:00 2001 From: Jaesung Lee Date: Wed, 2 Aug 2023 04:04:57 +0900 Subject: [PATCH 2/2] [ADD] Added `scrolledToEndPublisher`. This sends event when the list is scrolled to the end --- .../Publishers/ScrolledToEndPublisher.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Sources/ChatUI/Publishers/ScrolledToEndPublisher.swift diff --git a/Sources/ChatUI/Publishers/ScrolledToEndPublisher.swift b/Sources/ChatUI/Publishers/ScrolledToEndPublisher.swift new file mode 100644 index 0000000..180fc2d --- /dev/null +++ b/Sources/ChatUI/Publishers/ScrolledToEndPublisher.swift @@ -0,0 +1,30 @@ +// +// ScrolledToEndPublisher.swift +// +// +// Created by Jaesung Lee on 2023/03/19. +// + +import Combine + +// TODO: Unstable + +/** + The publisher that sends event when the list is scrolled to the end. + + ```swift + // How to publish + scrolledToEndPublisher.send(true) + ``` + ```swift + // How to subscribe + .onReceive(scrolledToEndPublisher) { isEnded in + if isEnded { + loadMoreMessages() + } + } + ``` + + - Important: This publisher is the beta feature. + */ +public var scrolledToEndPublisher = PassthroughSubject()