Exyte Chat

repository·main·Indexed 23 days ago

https://github.com/exyte/chat

A SwiftUI framework for building highly customizable chat interfaces. It includes built-in support for media picking, sticker keyboards, message menus (reply, edit, delete), and various content types such as markdown, links, and attachments. The framework provides flexible components like ChatView, custom message and input view builders, and support for different chat types (.conversation, .comments) and reply modes (.quote, .answer).

Tokens
6.3K
Snippets
12
Records
22
Agent score
33%

What's inside exyte-chat

  1. Handle large attachment uploads and progress

    main

    The library supports uploading attachments larger than 100 MB. You can control how upload progress is displayed using the fullUploadStatus property on an Attachment object. There are three implementation strategies:

    1. No status (Default): Attachment(fullUploadStatus: nil). Shows no progress indicators. Best if the file is fully uploaded before the message is sent.
    2. Generic progress: Attachment(fullUploadStatus: .inProgress(nil)). Shows a generic progress indicator for both sender and receiver without a percentage.
    3. Percentage progress: Attachment(fullUploadStatus: .inProgress(0)). Shows a specific percentage. Requires the client to synchronize multiple WebSocket updates (e.g., 10%, 20%) between sender and receiver.

    To manage the lifecycle of the progress indicator, the client must handle these status updates:

    • Complete: Attachment(fullUploadStatus: .complete)
    • Cancelled: Attachment(fullUploadStatus: .cancelled)
    • Error: Attachment(fullUploadStatus: .error)
  2. Configure ChatType and ReplyMode

    main

    You can control the message flow and how replies are displayed by passing chatType and replyMode to the ChatView initializer.

    Chat Types

    • .conversation: Latest messages are at the bottom; new messages animate from the bottom.
    • .comments: Latest messages are at the top; new messages animate from the top.

    Reply Modes

    • .quote: When replying to a message, the new message appears as a standard message with the original message quoted in its body.
    • .answer: When replying, the new message appears directly below the original message as a separate cell without duplicating the original content in the body.
    ChatView(messages: viewModel.messages, chatType: .comments, replyMode: .answer) { draft in
        yourViewModel.send(draft: draft)
    }
  3. Quickstart: Create a basic ChatView

    main

    To implement a basic chat interface, use ChatView with a list of Message objects and a closure to handle sending new messages. The Message type is provided by the library and stores text as an AttributedString. You can map your own backend models to Message or use them directly.

    @State var messages: [Message] = []
    
    var body: some View {
        ChatView(messages: messages) { draft in
            yourViewModel.send(draft: draft)
        }
    }
  4. Implement a custom message menu

    main

    To define custom actions for the message menu (triggered by a long tap), declare an enum that conforms to MessageMenuAction. You can provide custom titles, icons, and determine if an action is destructive (showing a confirmation alert) or conditionally available per message.

    When initializing ChatView, pass your custom enum type to the messageMenuAction closure. This closure provides:

    • selectedMenuAction: The specific case selected by the user (ensure you explicitly type this variable with your enum type).
    • defaultActionClosure: A helper to trigger standard behaviors like .copy, .reply, .edit, or .share. Note that for .edit, you must provide a closure to handle the updated text.
    • message: The message associated with the menu.
    enum Action: MessageMenuAction {
        case reply, edit, delete
    
        func title() -> String {
            switch self {
            case .reply: "Reply"
            case .edit: "Edit"
            case .delete: "Delete"
            }
        }
        
        func icon() -> Image {
            switch self {
            case .reply: Image(systemName: "arrowshape.turn.up.left")
            case .edit: Image(systemName: "square.and.pencil")
            case .delete: Image(systemName: "trash")
            }
        }
    
        func isDestructive() -> Bool { self == .delete }
    
        static func menuItems(for message: ExyteChat.Message) -> [Action] {
            message.user.isCurrentUser ? [.reply, .edit, .delete] : [.reply]
        }
    }
    
    ChatView(messages: viewModel.messages) { draft in
        viewModel.send(draft: draft)
    } messageMenuAction: { (action: Action, defaultActionClosure, message) in
        switch action {
        case .reply:
            defaultActionClosure(message, .reply)
        case .edit:
            defaultActionClosure(message, .edit { editedText in
                print(editedText)
            })
        case .delete:
            yourViewModel.delete(message: message)
        }
    }
  5. Set up the Firestore Chat Example

    main

    The repository includes an example project integrated with Firebase/Firestore. To run it with your own data, follow these steps:

    1. Create a Firebase app at https://console.firebase.google.com/
    2. Create a Firestore database to manage lightweight text data.
    3. Create a Cloud Storage bucket to handle media like images and voice recordings.
    4. Replace the GoogleService-Info.plist in the project with your own configuration file.

    Once configured, you can test the chat functionality across multiple simulators or physical devices.

  6. Install Chat via Swift Package Manager

    main

    Add the Chat repository as a dependency in your Package.swift file to use the library in your Swift projects.

    dependencies: [
        .package(url: "https://github.com/exyte/Chat.git")
    ]
  7. Migrate to version 3

    main

    If you are upgrading from a previous version to version 3, note the following breaking changes:

    • enableLoadMore(offset...) replaces enableLoadMore(pageSize...). The trailing closure no longer accepts arguments.
    • linkPreviewsDisabled replaces linkPreviewsEnabled.
    • shouldShowPreviewForLink replaces shouldShowLinkPreview.
    • messageUseMarkdown and messageUseStyler have been removed. Messages now use markdown and underline links by default. To use custom attributes, initialize Message with an AttributedString directly.
    • The closures for messageBuilder and inputViewBuilder now each receive a single struct (different types for each) instead of multiple arguments.
  8. Run the Chat Examples

    main

    You can explore the library's capabilities by running the provided example projects:

    1. Clone the repository: https://github.com/exyte/Chat.git
    2. Open ChatExample.xcodeproj (for a simple bot demo) or ChatFirestoreExample.xcodeproj (for the full Firebase integration) in Xcode.
    3. Build and run the project.
  9. Customize chat theme colors and images

    main

    Use the .chatTheme modifier to customize the colors and images of the default UI. You can provide a ChatTheme object containing colors and images configurations.

    // Customize colors and images
    .chatTheme(
        ChatTheme(
            colors: .init(
                mainBackground: .red,
                buttonBackground: .yellow,
                addButtonBackground: .purple
            ),
            images: .init(
                camera: Image(systemName: "camera")
            )
        )
    )
    
    // Chat view with a full background image  
    .chatTheme(
        ChatTheme(
            colors: .init(
                buttonBackground: .yellow,
                addButtonBackground: .purple
            ),
            images: .init(
                background: ChatTheme.Images.Background(
                    portraitBackgroundLight: Image("chatBackgroundLight"),
                    portraitBackgroundDark: Image("chatBackgroundDark"),
                    landscapeBackgroundLight: Image("chatBackgroundLandscapeLight"),
                    landscapeBackgroundDark: Image("chatBackgroundLandscapeDark")
                )
            )
        )
    )
  10. Add custom swipe actions to messages

    main

    You can add swipe actions to messages in a ChatView using the .swipeActions modifier. Each SwipeAction requires an action to perform, an optional activeFor closure to determine visibility (e.g., only showing 'Edit' for the current user), and a background color. The items parameter takes a list of SwipeAction objects.

    // Example: Adding Swipe Actions to your ChatView
    ChatView(messages: viewModel.messages) { draft in
        viewModel.send(draft: draft)
    } 
    .swipeActions(edge: .leading, performsFirstActionWithFullSwipe: false, items: [
        // SwipeActions are similar to Buttons, they accept an Action and a ViewBuilder
        SwipeAction(action: onDelete, activeFor: { $0.user.isCurrentUser }, background: .red) {
            swipeActionButtonStandard(title: "Delete", image: "xmark.bin")
        },
        // Set the background color of a SwipeAction in the initializer, 
        // instead of trying to apply a background color in your ViewBuilder
        SwipeAction(action: onReply, background: .blue) {
            swipeActionButtonStandard(title: "Reply", image: "arrowshape.turn.up.left")
        },
        // SwipeActions can also be selectively shown based on the message, 
        // here we only show the Edit action when the message is from the current sender
        SwipeAction(action: onEdit, activeFor: { $0.user.isCurrentUser }, background: .gray) {
            swipeActionButtonStandard(title: "Edit", image: "bubble.and.pencil")
        }
    ])
  11. Integrate Giphy sticker keyboard

    main

    To use the integrated sticker keyboard for animated GIFs, you must have a client ID from Giphy. You need to enable .giphy in setAvailableInputs and configure it using .giphyConfig.

    .setAvailableInputs([.text, .giphy])
    .giphyConfig(
        GiphyConfiguration(
            giphyKey: "client id",
            mediaTypeConfig: [.recents, .gifs, .stickers, .clips],
            showAttributionMark: true
        )
    )