Message Moderation

Stream Chat can moderate messages before they are delivered (bounce) and lets users report messages to moderators (flag). This page shows how to customize the SwiftUI components for moderated messages. For how moderated messages are represented in the SDK, see the Moderation guide.

Bounced Messages

A bounced message is displayed with an error indicator next to it. When the user long presses it, the SDK shows an alert with the Send Anyway, Edit Message and Delete Message actions. This is controlled by the bouncedMessagesAlertActionsEnabled flag of MessageListConfig, which is true by default.

The alert is added by the BouncedMessageActionsModifier. To provide your own UI, implement makeBouncedMessageActionsModifier in your custom Styles. The ChatChannelViewModel exposes the bounced message and the actions that can be performed on it:

struct CustomBouncedMessageActionsModifier: ViewModifier {
    @ObservedObject var viewModel: ChatChannelViewModel

    func body(content: Content) -> some View {
        content
            .alert(isPresented: $viewModel.bouncedActionsViewShown) {
                Alert(
                    title: Text("Message not sent"),
                    message: Text("This message goes against our community guidelines."),
                    primaryButton: .default(Text("Edit Message")) {
                        if let message = viewModel.bouncedMessage {
                            viewModel.editMessage(message)
                        }
                    },
                    secondaryButton: .destructive(Text("Delete Message")) {
                        if let message = viewModel.bouncedMessage {
                            viewModel.deleteMessage(message)
                        }
                    }
                )
            }
    }
}

final class CustomStyles: Styles {
    var composerPlacement: ComposerPlacement = .docked

    func makeBouncedMessageActionsModifier(viewModel: ChatChannelViewModel) -> some ViewModifier {
        CustomBouncedMessageActionsModifier(viewModel: viewModel)
    }

    func makeComposerInputViewModifier(options: ComposerInputModifierOptions) -> some ViewModifier {
        RegularInputViewModifier()
    }

    func makeComposerButtonViewModifier(options: ComposerButtonModifierOptions) -> some ViewModifier {
        RegularButtonViewModifier()
    }

    func makeSuggestionsContainerModifier(options: SuggestionsContainerModifierOptions) -> some ViewModifier {
        SuggestionsRegularContainerModifier()
    }
}

class CustomViewFactory: ViewFactory {
    @Injected(\.chatClient) public var chatClient
    public var styles = CustomStyles()
}

If you set bouncedMessagesAlertActionsEnabled to false, long pressing a bounced message opens the regular message actions overlay instead. In this case, the actions are provided by supportedMessageActions, as described in the next section, and you can check options.message.isBounced to return different actions for bounced messages.

Flag Action

The message actions are provided by the supportedMessageActions closure of MessageListConfig. The default flag action has the MessageActionId.flag identifier, so you can remove it or replace it with your own action:

let utils = Utils(
    messageListConfig: MessageListConfig(
        supportedMessageActions: { options in
            var actions = MessageAction.defaultActions(for: options)
            guard let index = actions.firstIndex(where: { $0.id == MessageActionId.flag }) else {
                return actions
            }

            let chatClient = InjectedValues[\.chatClient]
            let messageController = chatClient.messageController(
                cid: options.channel.cid,
                messageId: options.message.id
            )
            actions[index] = MessageAction(
                id: MessageActionId.flag,
                title: "Report",
                iconName: "exclamationmark.bubble",
                action: {
                    messageController.flag(reason: "inappropriate") { error in
                        if let error {
                            options.onError(error)
                        } else {
                            options.onFinish(MessageActionInfo(message: options.message, identifier: "flag"))
                        }
                    }
                },
                confirmationPopup: ConfirmationPopup(
                    title: "Report message?",
                    message: "Our moderators will review this message.",
                    buttonTitle: "Report"
                ),
                isDestructive: true
            )
            return actions
        }
    )
)
let streamChat = StreamChat(chatClient: chatClient, utils: utils)

To hide the flag action, filter it out instead: MessageAction.defaultActions(for: options).filter { $0.id != MessageActionId.flag }.

For more details, see View Customizations and Message Reactions.

Changing the Texts

If you only need to change the texts, override the following keys in your app's Localizable.strings:

Key Default value
message.moderation.alert.title Are you sure?
message.moderation.alert.message Consider how your comment might...
message.moderation.alert.resend Send Anyway
message.moderation.alert.edit Edit Message
message.moderation.alert.delete Delete Message
message.bounce.title Message was bounced
message.actions.flag Flag Message
message.actions.flag.confirmation-title Flag Message
message.actions.flag.confirmation-message Do you want to send a copy of...

See the localization guide for more details.

Add Chat to my app: getstream.io/SKILL.md

The fastest way to build with Stream. Start a new project or improve an existing one. Full CLI and documentation integration out of the box.


Ask your agent:

/stream Build me a Social App with Feeds and Moderation.
/stream Any livestream calls running?
/stream Chat iOS v5: <Your Question>