Skip to content

Commit e6be9e2

Browse files
Add support for bounced message alert actions (#764)
* Add new moderation localization keys * Add support for bounced message alert actions * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: Toomas Vahter <[email protected]> * PR feedback changes * Update CHANGELOG.md * Update CHANGELOG.md --------- Co-authored-by: Toomas Vahter <[email protected]>
1 parent 059cb06 commit e6be9e2

File tree

11 files changed

+186
-6
lines changed

11 files changed

+186
-6
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6+
### ✅ Added
7+
- Add `Utils.MessageListConfig.bouncedMessagesAlertActionsEnabled` to support bounced actions alert [#764](https://github.com/GetStream/stream-chat-swiftui/pull/764)
8+
- Add `ViewFactory.makeBouncedMessageActionsModifier()` to customize the new bounced actions alert [#764](https://github.com/GetStream/stream-chat-swiftui/pull/764)
69
### 🐞 Fixed
710
- Fix visibility of tabbar when reactions are shown [#750](https://github.com/GetStream/stream-chat-swiftui/pull/750)
811
- Show all members in direct message channel info view [#760](https://github.com/GetStream/stream-chat-swiftui/pull/760)
912
### 🔄 Changed
1013
- Only show "Pin/Unpin message" Action if user has permission [#749](https://github.com/GetStream/stream-chat-swiftui/pull/749)
1114
- Filter deactivated users in channel info view [#758](https://github.com/GetStream/stream-chat-swiftui/pull/758)
15+
- Bounced message actions will now be shown as an alert instead of a context menu by default [#764](https://github.com/GetStream/stream-chat-swiftui/pull/764)
16+
### 🎭 New Localizations
17+
Add localizable keys for supporting moderation alerts:
18+
- `message.moderation.alert.title`
19+
- `message.moderation.alert.message`
20+
- `message.moderation.alert.resend`
21+
- `message.moderation.alert.edit`
22+
- `message.moderation.alert.delete`
23+
- `message.moderation.alert.cancel`
1224

1325
# [4.72.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.72.0)
1426
_February 04, 2025_

DemoAppSwiftUI/AppDelegate.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
6666
messageListConfig: MessageListConfig(
6767
dateIndicatorPlacement: .messageList,
6868
userBlockingEnabled: true,
69+
bouncedMessagesAlertActionsEnabled: true,
6970
skipEditedMessageLabel: { message in
7071
message.extraData["ai_generated"]?.boolValue == true
7172
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import StreamChat
6+
import SwiftUI
7+
8+
/// The modifier that shows the actions for a bounced message.
9+
///
10+
/// This modifier is only used if `Utils.messageListConfig.bouncedMessagesAlertActionsEnabled` is `true`.
11+
public struct BouncedMessageActionsModifier: ViewModifier {
12+
@ObservedObject private var viewModel: ChatChannelViewModel
13+
14+
public init(
15+
viewModel: ChatChannelViewModel
16+
) {
17+
self.viewModel = viewModel
18+
}
19+
20+
public func body(content: Content) -> some View {
21+
if #available(iOS 15.0, *) {
22+
content
23+
.alert(
24+
L10n.Message.Moderation.Alert.title,
25+
isPresented: $viewModel.bouncedActionsViewShown
26+
) {
27+
Button(L10n.Message.Moderation.Alert.resend) {
28+
resendBouncedMessage()
29+
}
30+
Button(L10n.Message.Moderation.Alert.edit) {
31+
editBouncedMessage()
32+
}
33+
Button(L10n.Message.Moderation.Alert.delete, role: .destructive) {
34+
deleteBouncedMessage()
35+
}
36+
Button(L10n.Message.Moderation.Alert.cancel, role: .cancel, action: {
37+
viewModel.bouncedActionsViewShown = false
38+
})
39+
} message: {
40+
Text(L10n.Message.Moderation.Alert.message)
41+
}
42+
} else {
43+
content
44+
.actionSheet(isPresented: $viewModel.bouncedActionsViewShown) {
45+
ActionSheet(
46+
title: Text(L10n.Message.Moderation.Alert.title),
47+
message: Text(L10n.Message.Moderation.Alert.message),
48+
buttons: [
49+
.default(Text(L10n.Message.Moderation.Alert.resend)) {
50+
resendBouncedMessage()
51+
},
52+
.default(Text(L10n.Message.Moderation.Alert.edit)) {
53+
editBouncedMessage()
54+
},
55+
.destructive(Text(L10n.Message.Moderation.Alert.delete)) {
56+
deleteBouncedMessage()
57+
},
58+
.cancel(Text(L10n.Message.Moderation.Alert.cancel))
59+
]
60+
)
61+
}
62+
}
63+
}
64+
65+
private func editBouncedMessage() {
66+
guard let bouncedMessage = viewModel.bouncedMessage else {
67+
return
68+
}
69+
viewModel.editMessage(bouncedMessage)
70+
}
71+
72+
private func resendBouncedMessage() {
73+
guard let bouncedMessage = viewModel.bouncedMessage else {
74+
return
75+
}
76+
viewModel.resendMessage(bouncedMessage)
77+
}
78+
79+
private func deleteBouncedMessage() {
80+
guard let bouncedMessage = viewModel.bouncedMessage else {
81+
return
82+
}
83+
viewModel.deleteMessage(bouncedMessage)
84+
}
85+
}

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,14 @@ public struct ChatChannelView<Factory: ViewFactory>: View, KeyboardReadable {
6161
onMessageAppear: viewModel.handleMessageAppear(index:scrollDirection:),
6262
onScrollToBottom: viewModel.scrollToLastMessage,
6363
onLongPress: { displayInfo in
64-
messageDisplayInfo = displayInfo
65-
withAnimation {
66-
viewModel.showReactionOverlay(for: AnyView(self))
64+
let isBouncedAlertEnabled = utils.messageListConfig.bouncedMessagesAlertActionsEnabled
65+
if isBouncedAlertEnabled && displayInfo.message.isBounced {
66+
viewModel.showBouncedActionsView(for: displayInfo.message)
67+
} else {
68+
messageDisplayInfo = displayInfo
69+
withAnimation {
70+
viewModel.showReactionOverlay(for: AnyView(self))
71+
}
6772
}
6873
},
6974
onJumpToMessage: viewModel.jumpToMessage(messageId:)
@@ -196,6 +201,7 @@ public struct ChatChannelView<Factory: ViewFactory>: View, KeyboardReadable {
196201
.alertBanner(isPresented: $viewModel.showAlertBanner)
197202
.accessibilityElement(children: .contain)
198203
.accessibilityIdentifier("ChatChannelView")
204+
.modifier(factory.makeBouncedMessageActionsModifier(viewModel: viewModel))
199205
}
200206

201207
private var generatingSnapshot: Bool {

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
8989
}
9090
}
9191

92+
@Published public var bouncedMessage: ChatMessage?
93+
@Published public var bouncedActionsViewShown = false {
94+
didSet {
95+
if bouncedActionsViewShown == false {
96+
bouncedMessage = nil
97+
}
98+
}
99+
}
100+
92101
@Published public var quotedMessage: ChatMessage? {
93102
didSet {
94103
if oldValue != nil && quotedMessage == nil {
@@ -451,7 +460,28 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
451460
public func showReactionOverlay(for view: AnyView) {
452461
currentSnapshot = utils.snapshotCreator.makeSnapshot(for: view)
453462
}
454-
463+
464+
public func showBouncedActionsView(for message: ChatMessage) {
465+
bouncedActionsViewShown = true
466+
bouncedMessage = message
467+
}
468+
469+
public func deleteMessage(_ message: ChatMessage) {
470+
guard let cid = message.cid else { return }
471+
let messageController = chatClient.messageController(cid: cid, messageId: message.id)
472+
messageController.deleteMessage()
473+
}
474+
475+
public func resendMessage(_ message: ChatMessage) {
476+
guard let cid = message.cid else { return }
477+
let messageController = chatClient.messageController(cid: cid, messageId: message.id)
478+
messageController.resendMessage()
479+
}
480+
481+
public func editMessage(_ message: ChatMessage) {
482+
messageActionExecuted(.init(message: message, identifier: "edit"))
483+
}
484+
455485
public func messageActionExecuted(_ messageActionInfo: MessageActionInfo) {
456486
utils.messageActionsResolver.resolveMessageAction(
457487
info: messageActionInfo,

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListConfig.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public struct MessageListConfig {
3232
isMessageEditedLabelEnabled: Bool = true,
3333
markdownSupportEnabled: Bool = true,
3434
userBlockingEnabled: Bool = false,
35+
bouncedMessagesAlertActionsEnabled: Bool = true,
3536
skipEditedMessageLabel: @escaping (ChatMessage) -> Bool = { _ in false }
3637
) {
3738
self.messageListType = messageListType
@@ -57,6 +58,7 @@ public struct MessageListConfig {
5758
self.isMessageEditedLabelEnabled = isMessageEditedLabelEnabled
5859
self.markdownSupportEnabled = markdownSupportEnabled
5960
self.userBlockingEnabled = userBlockingEnabled
61+
self.bouncedMessagesAlertActionsEnabled = bouncedMessagesAlertActionsEnabled
6062
self.skipEditedMessageLabel = skipEditedMessageLabel
6163
}
6264

@@ -83,6 +85,12 @@ public struct MessageListConfig {
8385
public let isMessageEditedLabelEnabled: Bool
8486
public let markdownSupportEnabled: Bool
8587
public let userBlockingEnabled: Bool
88+
89+
/// A boolean to enable the alert actions for bounced messages.
90+
///
91+
/// By default it is true and the bounced actions are displayed as an alert instead of a context menu.
92+
public let bouncedMessagesAlertActionsEnabled: Bool
93+
8694
public let skipEditedMessageLabel: (ChatMessage) -> Bool
8795
}
8896

@@ -111,7 +119,6 @@ public enum DateIndicatorPlacement {
111119

112120
/// Used to show and hide different helper views around the message.
113121
public struct MessageDisplayOptions {
114-
115122
public let showAvatars: Bool
116123
public let showAvatarsInGroups: Bool
117124
public let showMessageDate: Bool

Sources/StreamChatSwiftUI/DefaultViewFactory.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,11 @@ extension ViewFactory {
270270
forceLeftToRight: messageModifierInfo.forceLeftToRight
271271
)
272272
}
273-
273+
274+
public func makeBouncedMessageActionsModifier(viewModel: ChatChannelViewModel) -> some ViewModifier {
275+
BouncedMessageActionsModifier(viewModel: viewModel)
276+
}
277+
274278
public func makeEmptyMessagesView(
275279
for channel: ChatChannel,
276280
colors: ColorPalette

Sources/StreamChatSwiftUI/Generated/L10n.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,22 @@ internal enum L10n {
436436
/// GIPHY
437437
internal static var title: String { L10n.tr("Localizable", "message.giphy-attachment.title") }
438438
}
439+
internal enum Moderation {
440+
internal enum Alert {
441+
/// Cancel
442+
internal static var cancel: String { L10n.tr("Localizable", "message.moderation.alert.cancel") }
443+
/// Delete Message
444+
internal static var delete: String { L10n.tr("Localizable", "message.moderation.alert.delete") }
445+
/// Edit Message
446+
internal static var edit: String { L10n.tr("Localizable", "message.moderation.alert.edit") }
447+
/// Consider how your comment might make others feel and be sure to follow our Community Guidelines.
448+
internal static var message: String { L10n.tr("Localizable", "message.moderation.alert.message") }
449+
/// Send Anyway
450+
internal static var resend: String { L10n.tr("Localizable", "message.moderation.alert.resend") }
451+
/// Are you sure?
452+
internal static var title: String { L10n.tr("Localizable", "message.moderation.alert.title") }
453+
}
454+
}
439455
internal enum Polls {
440456
/// Anonymous
441457
internal static var unknownVoteAuthor: String { L10n.tr("Localizable", "message.polls.unknown-vote-author") }

Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@
5151
"message.actions.user-block.confirmation-message" = "Are you sure you want to block this user?";
5252
"message.actions.user-unblock.confirmation-message" = "Are you sure you want to unblock this user?";
5353

54+
"message.moderation.alert.title" = "Are you sure?";
55+
"message.moderation.alert.message" = "Consider how your comment might make others feel and be sure to follow our Community Guidelines.";
56+
"message.moderation.alert.resend" = "Send Anyway";
57+
"message.moderation.alert.edit" = "Edit Message";
58+
"message.moderation.alert.delete" = "Delete Message";
59+
"message.moderation.alert.cancel" = "Cancel";
60+
5461
"messageList.typingIndicator.typing-unknown" = "Someone is typing";
5562

5663
"message.threads.reply" = "Thread Reply";

Sources/StreamChatSwiftUI/ViewFactory.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,14 @@ public protocol ViewFactory: AnyObject {
254254
/// - Parameter messageModifierInfo: the message modifier info, that will be applied to the message.
255255
func makeMessageViewModifier(for messageModifierInfo: MessageModifierInfo) -> MessageViewModifier
256256

257+
associatedtype BouncedMessageActionsModifierType: ViewModifier
258+
/// Returns a view modifier applied to the bounced message actions.
259+
///
260+
/// This modifier is only used if `Utils.messageListConfig.bouncedMessagesAlertActionsEnabled` is `true`.
261+
/// By default the flag is true and the bounced actions are shown as an alert instead of a context menu.
262+
/// - Parameter viewModel: the view model of the chat channel view.
263+
func makeBouncedMessageActionsModifier(viewModel: ChatChannelViewModel) -> BouncedMessageActionsModifierType
264+
257265
associatedtype UserAvatar: View
258266
/// Creates the message avatar view.
259267
/// - Parameter userDisplayInfo: the author's display info.

0 commit comments

Comments
 (0)