Skip to content

Commit

Permalink
Add .edit and .delete to message menu
Browse files Browse the repository at this point in the history
  • Loading branch information
f3dm76 committed Jul 5, 2024
1 parent 47030b0 commit 695e91b
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
"version" : "2.1.1"
}
},
{
"identity" : "popupview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/exyte/PopupView.git",
"state" : {
"revision" : "259f45a4fcc42ea4ebb3ab61e8d6d6dfc4f652ed",
"version" : "3.0.3"
}
},
{
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
Expand Down
15 changes: 15 additions & 0 deletions Example/ChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//

import SwiftUI
import ExyteMediaPicker

struct ContentView: View {
var body: some View {
Expand Down Expand Up @@ -31,5 +32,19 @@ struct ContentView: View {
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(.stack)

.mediaPickerTheme(
main: .init(
text: .white,
albumSelectionBackground: .examplePickerBg,
fullscreenPhotoBackground: .examplePickerBg
),
selection: .init(
emptyTint: .white,
emptyBackground: .black.opacity(0.25),
selectedTint: .exampleBlue,
fullscreenTint: .white
)
)
}
}
13 changes: 0 additions & 13 deletions Example/ChatExample/Screens/ChatExampleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,6 @@ struct ChatExampleView: View {
status: viewModel.chatStatus,
cover: viewModel.chatCover
)
.mediaPickerTheme(
main: .init(
text: .white,
albumSelectionBackground: .examplePickerBg,
fullscreenPhotoBackground: .examplePickerBg
),
selection: .init(
emptyTint: .white,
emptyBackground: .black.opacity(0.25),
selectedTint: .exampleBlue,
fullscreenTint: .white
)
)
.onAppear(perform: viewModel.onStart)
.onDisappear(perform: viewModel.onStop)
}
Expand Down
47 changes: 47 additions & 0 deletions Example/ChatExample/Screens/CommentsExampleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,36 @@
import SwiftUI
import ExyteChat

enum Action: MessageMenuAction {
case reply, edit, delete, print

func title() -> String {
switch self {
case .reply:
"Reply"
case .edit:
"Edit"
case .delete:
"Delete"
case .print:
"Print"
}
}

func icon() -> Image {
switch self {
case .reply:
Image(systemName: "arrowshape.turn.up.left")
case .edit:
Image(systemName: "square.and.pencil")
case .delete:
Image(systemName: "xmark.bin")
case .print:
Image(systemName: "printer")
}
}
}

struct CommentsExampleView: View {

@StateObject var viewModel = CommentsExampleViewModel()
Expand All @@ -28,6 +58,23 @@ struct CommentsExampleView: View {
viewModel.send(draft: draft)
} messageBuilder: { message, positionInGroup, positionInCommentsGroup, showContextMenuClosure, messageActionClosure, showAttachmentClosure in
messageCell(message, positionInCommentsGroup, showMenuClosure: showContextMenuClosure, actionClosure: messageActionClosure, attachmentClosure: showAttachmentClosure)
} messageMenuAction: { (action: Action, defaultActionClosure, message) in
switch action {
case .reply:
defaultActionClosure(message, .reply)
case .edit:
defaultActionClosure(message, .edit { editedText in
// update this message's text on your BE
print(editedText)
})
case .delete:
defaultActionClosure(message, .delete(confirmClosure: {
// delete this message on your BE
print("deleted")
}))
case .print:
print(message.text)
}
}
.showDateHeaders(false)
}
Expand Down
7 changes: 6 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ let package = Package(
url: "https://github.com/exyte/ActivityIndicatorView",
from: "1.0.0"
),
.package(
url: "https://github.com/exyte/PopupView.git",
from: "3.0.0"
),
],
targets: [
.target(
Expand All @@ -38,7 +42,8 @@ let package = Package(
.product(name: "SwiftUIIntrospect", package: "swiftui-introspect"),
.product(name: "ExyteMediaPicker", package: "MediaPicker"),
.product(name: "FloatingButton", package: "FloatingButton"),
.product(name: "ActivityIndicatorView", package: "ActivityIndicatorView")
.product(name: "ActivityIndicatorView", package: "ActivityIndicatorView"),
.product(name: "PopupView", package: "PopupView")
]
),
.testTarget(
Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,52 @@ ChatView(messages: viewModel.messages) { draft in
- `inputViewActionClosure` for calling on taps on your custom buttons. For example, call `inputViewActionClosure(.send)` if you want to send your message with your own button, then the library will reset the text and attachments and call the `didSendMessage` sending closure
- `dismissKeyboardClosure` - call this to dismiss keyboard

## Custom message menu
Long tap on a message will display a menu for this message (can be turned off, see Modifiers). To define custom message menu actions declare an enum conforming to `MessageMenuAction`. Then the library will show your custom menu options on long tap on message instead of default ones, if you pass your enum's name to it (see code sample). Once the action is selected special callbcak will be called. Here is a simple example:
```swift
enum Action: MessageMenuAction {
case reply, edit

func title() -> String {
switch self {
case .reply:
"Reply"
case .edit:
"Edit"
}
}

func icon() -> Image {
switch self {
case .reply:
Image(systemName: "arrowshape.turn.up.left")
case .edit:
Image(systemName: "square.and.pencil")
}
}
}

ChatView(messages: viewModel.messages) { draft in
viewModel.send(draft: draft)
} messageMenuAction: { (action: Action, defaultActionClosure, message) in // <-- here: specify the name of your `MessageMenuAction` enum
switch action {
case .reply:
defaultActionClosure(message, .reply)
case .edit:
defaultActionClosure(message, .edit { editedText in
// update this message's text on your BE
print(editedText)
})
}
}
```
`messageMenuAction`'s parameters:
- `selectedMenuAction` - action selected by the user from the menu. NOTE: when declaring this variable, specify its type (your custom descendant of MessageMenuAction) explicitly
- `defaultActionClosure` - a closure taking a case of default implementation of MessageMenuAction which provides simple actions handlers; you call this closure passing the selected message and choosing one of the default actions if you need them; or you can write a custom implementation for all your actions, in that case just ignore this closure
- `message` - message for which the menu is displayed

When implementing your own `MessageMenuActionClosure`, write a switch statement passing through all the cases of your `MessageMenuAction`, inside each case write your own action handler, or call the default one. NOTE: not all default actions work out of the box - e.g. for `.edit` you'll still need to provide a closure to save the edited text on your BE. Please see CommentsExampleView in ChatExample project for MessageMenuActionClosure usage example.

## Small view builders:
These use `AnyView`, so please try to keep them easy enough
- `betweenListAndInputViewBuilder` - content to display in between the chat list view and the input view
Expand Down
53 changes: 46 additions & 7 deletions Sources/ExyteChat/ChatView/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SwiftUI
import FloatingButton
import SwiftUIIntrospect
import ExyteMediaPicker
import PopupView

public typealias MediaPickerParameters = SelectionParamsHolder

Expand Down Expand Up @@ -55,11 +56,16 @@ public struct ChatView<MessageContent: View, InputViewContent: View, MenuAction:
_ dismissKeyboardClosure: ()->()
) -> InputViewContent

/// To define custom message menu actions
/// - enum listing action options
/// To define custom message menu actions declare an enum conforming to MessageMenuAction. The library will show your custom menu options on long tap on message. Once the action is selected the following callback will be called:
/// - action selected by the user from the menu. NOTE: when declaring this variable, specify its type (your custom descendant of MessageMenuAction) explicitly
/// - a closure taking a case of default implementation of MessageMenuAction which provides simple actions handlers; you call this closure passing the selected message and choosing one of the default actions if you need them; or you can write a custom implementation for all your actions, in that case just ignore this closure
/// - message for which the menu is displayed
/// closure returns the action to perform on selected action tap
public typealias MessageMenuActionClosure = ((MenuAction, Message)->Void)
/// When implementing your own MessageMenuActionClosure, write a switch statement passing through all the cases of your MessageMenuAction, inside each case write your own action handler, or call the default one. NOTE: not all default actions work out of the box - e.g. for .edit you'll still need to provide a closure to save the edited text on your BE. Please see CommentsExampleView in ChatExample project for MessageMenuActionClosure usage example.
public typealias MessageMenuActionClosure = (
_ selectedMenuAction: MenuAction,
_ defaultActionClosure: @escaping (Message, DefaultMessageMenuAction) -> Void,
_ message: Message
) -> Void

/// User and MessageId
public typealias TapAvatarClosure = (User, String) -> ()
Expand Down Expand Up @@ -153,6 +159,8 @@ public struct ChatView<MessageContent: View, InputViewContent: View, MenuAction:
public var body: some View {
mainView
.background(theme.colors.mainBackground)
.environmentObject(keyboardState)

.fullScreenCover(isPresented: $viewModel.fullscreenAttachmentPresented) {
let attachments = sections.flatMap { section in section.rows.flatMap { $0.message.attachments } }
let index = attachments.firstIndex { $0.id == viewModel.fullscreenAttachmentItem?.id }
Expand All @@ -171,16 +179,47 @@ public struct ChatView<MessageContent: View, InputViewContent: View, MenuAction:
.ignoresSafeArea()
}
}

.fullScreenCover(isPresented: $inputViewModel.showPicker) {
AttachmentsEditor(inputViewModel: inputViewModel, inputViewBuilder: inputViewBuilder, chatTitle: chatTitle, messageUseMarkdown: messageUseMarkdown, orientationHandler: orientationHandler, mediaPickerSelectionParameters: mediaPickerSelectionParameters, availableInput: availablelInput)
.environmentObject(globalFocusState)
}

.onChange(of: inputViewModel.showPicker) {
if $0 {
globalFocusState.focus = nil
}
}
.environmentObject(keyboardState)

.popup(isPresented: $viewModel.showConfirmDeleteMessage) {
VStack(spacing: 0) {
Text("Are you sure you want to delete this message?")
.multilineTextAlignment(.center)
.padding(15)

Divider()

Button("Delete", role: .destructive) {
viewModel.confirmDeleteMessageClosure?()
viewModel.showConfirmDeleteMessage = false
}
.padding(15)

Divider()

Button("Cancel", role: .cancel) {
viewModel.confirmDeleteMessageClosure = nil
viewModel.showConfirmDeleteMessage = false
}
.padding(15)
}
.background(.ultraThickMaterial)
.cornerRadius(10)
.padding(.horizontal, 30)
} customize: {
$0.type(.floater())
.closeOnTap(false)
}
}

var mainView: some View {
Expand Down Expand Up @@ -253,7 +292,7 @@ public struct ChatView<MessageContent: View, InputViewContent: View, MenuAction:
UIList(viewModel: viewModel,
inputViewModel: inputViewModel,
isScrolledToBottom: $isScrolledToBottom,
shouldScrollToTop: $shouldScrollToTop,
shouldScrollToTop: $shouldScrollToTop,
tableContentHeight: $tableContentHeight,
messageBuilder: messageBuilder,
mainHeaderBuilder: mainHeaderBuilder,
Expand Down Expand Up @@ -378,7 +417,7 @@ public struct ChatView<MessageContent: View, InputViewContent: View, MenuAction:
if let messageMenuAction {
return { action in
hideMessageMenu()
messageMenuAction(action, message)
messageMenuAction(action, viewModel.messageMenuAction(), message)
}
} else if MenuAction.self == DefaultMessageMenuAction.self {
return { action in
Expand Down
18 changes: 15 additions & 3 deletions Sources/ExyteChat/ChatView/ChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ final class ChatViewModel: ObservableObject {
var inputViewModel: InputViewModel?
var globalFocusState: GlobalFocusState?

@Published var showConfirmDeleteMessage = false
@Published var confirmDeleteMessageClosure: (() -> Void)?

func presentAttachmentFullScreen(_ attachment: Attachment) {
fullscreenAttachmentItem = attachment
fullscreenAttachmentPresented = true
Expand All @@ -33,17 +36,26 @@ final class ChatViewModel: ObservableObject {
}

func messageMenuAction() -> (Message, DefaultMessageMenuAction) -> Void {
{ [weak self] in
self?.messageMenuActionInternal(message: $0, action: $1)
{ [weak self] message, action in
DispatchQueue.main.async {
self?.messageMenuActionInternal(message: message, action: action)
}
}
}

@MainActor
func messageMenuActionInternal(message: Message, action: DefaultMessageMenuAction) {
switch action {
case .reply:
inputViewModel?.attachments.replyMessage = message.toReplyMessage()
globalFocusState?.focus = .uuid(inputFieldId)
case .edit(let saveClosure):
inputViewModel?.text = message.text
inputViewModel?.edit(saveClosure)
globalFocusState?.focus = .uuid(inputFieldId)
case .delete(let confirmClosure):
showConfirmDeleteMessage = true
confirmDeleteMessageClosure = confirmClosure
}
}

}
Loading

0 comments on commit 695e91b

Please sign in to comment.