Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for sending system messages client-side #3477

Merged
merged 10 commits into from
Oct 30, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## StreamChat
### ✅ Added
- Add support for system messages not updating `channel.lastMessageAt` [#3476](https://github.com/GetStream/stream-chat-swift/pull/3476)
- Add support for sending system messages client-side
[#3477](https://github.com/GetStream/stream-chat-swift/pull/3477)
### 🐞 Fixed
- Fix watching channels when performing channel search [#3472](https://github.com/GetStream/stream-chat-swift/pull/3472)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,15 @@ final class DemoChatChannelListRouter: ChatChannelListRouter {
channelController.createNewMessage(text: message, skipPush: true)
}
}),
.init(title: "Send system message", isEnabled: canSendMessage, handler: { [unowned self] _ in
self.rootViewController.presentAlert(title: "Enter the message text", textFieldPlaceholder: "Send message") { message in
guard let message = message, !message.isEmpty else {
self.rootViewController.presentAlert(title: "Message is not valid")
return
}
channelController.createSystemMessage(text: message)
}
}),
.init(title: "Send message without url enriching", isEnabled: canSendMessage, handler: { [unowned self] _ in
self.rootViewController.presentAlert(title: "Enter the message text", textFieldPlaceholder: "Send message") { message in
guard let message = message, !message.isEmpty else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ struct MessageRequestBody: Encodable {
let id: String
let user: UserRequestBody
let text: String

// Used at the moment only for creating a system a message.
let type: String?

let command: String?
let args: String?
let parentId: String?
Expand All @@ -269,6 +273,7 @@ struct MessageRequestBody: Encodable {
id: String,
user: UserRequestBody,
text: String,
type: String? = nil,
command: String? = nil,
args: String? = nil,
parentId: String? = nil,
Expand All @@ -285,6 +290,7 @@ struct MessageRequestBody: Encodable {
self.id = id
self.user = user
self.text = text
self.type = type
self.command = command
self.args = args
self.parentId = parentId
Expand Down Expand Up @@ -312,6 +318,7 @@ struct MessageRequestBody: Encodable {
try container.encodeIfPresent(pinExpires, forKey: .pinExpires)
try container.encode(isSilent, forKey: .isSilent)
try container.encodeIfPresent(pollId, forKey: .pollId)
try container.encodeIfPresent(type, forKey: .type)

if !attachments.isEmpty {
try container.encode(attachments, forKey: .attachments)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,53 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
completion: completion
)
}


/// Sends a system message to the channel.
///
/// - Parameters:
/// - text: The text of the system message.
/// - messageId: The id for the sent message. By default, it is automatically generated by Stream.
/// - extraData: The extra data for the message.
/// - completion: Called when saving the message to the local DB finishes.
public func createSystemMessage(
text: String,
messageId: MessageId? = nil,
extraData: [String: RawJSON] = [:],
completion: ((Result<MessageId, Error>) -> Void)? = nil
) {
guard let cid = cid, isChannelAlreadyCreated else {
channelModificationFailed { error in
completion?(.failure(error ?? ClientError.Unknown()))
}
return
}

updater.createNewMessage(
in: cid,
messageId: messageId,
text: text,
pinning: nil,
isSilent: false,
isSystem: true,
command: nil,
arguments: nil,
attachments: [],
mentionedUserIds: [],
quotedMessageId: nil,
skipPush: false,
skipEnrichUrl: false,
poll: nil,
extraData: extraData
) { result in
if let newMessage = try? result.get() {
self.client.eventNotificationCenter.process(NewMessagePendingEvent(message: newMessage))
}
self.callback {
completion?(result.map(\.id))
}
}
}

/// Creates a new poll.
///
/// - Parameters:
Expand Down Expand Up @@ -1285,6 +1331,7 @@ public class ChatChannelController: DataController, DelegateCallable, DataStoreP
text: text,
pinning: pinning,
isSilent: isSilent,
isSystem: false,
command: nil,
arguments: nil,
attachments: attachments,
Expand Down
22 changes: 19 additions & 3 deletions Sources/StreamChat/Database/DTOs/MessageDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class MessageDTO: NSManagedObject {
@NSManaged var replyCount: Int32
@NSManaged var extraData: Data?
@NSManaged var isSilent: Bool

// Used for creating a message as a system message from the client SDK.
@NSManaged var isSystem: Bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it better to use the optional type directly here? If we need to use it for some other type, we would need to use another helper var like this one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinmitrevski I've removed the isSystem prop. The reason I added this, was because when using the message for the HTTP Request, we should only send the type if it is a system message, but now I check if the type is system, and if yes, then we send it. So the isSystem is not really needed 👍


@NSManaged var skipPush: Bool
@NSManaged var skipEnrichUrl: Bool
@NSManaged var isShadowed: Bool
Expand Down Expand Up @@ -621,6 +625,7 @@ extension NSManagedObjectContext: MessageDatabaseSession {
mentionedUserIds: [UserId],
showReplyInChannel: Bool,
isSilent: Bool,
isSystem: Bool,
quotedMessageId: MessageId?,
createdAt: Date?,
skipPush: Bool,
Expand Down Expand Up @@ -661,6 +666,10 @@ extension NSManagedObjectContext: MessageDatabaseSession {
message.parentMessageId = parentMessageId
message.extraData = try JSONEncoder.default.encode(extraData)
message.isSilent = isSilent
message.isSystem = isSystem
if isSystem {
message.type = MessageType.system.rawValue
nuno-vieira marked this conversation as resolved.
Show resolved Hide resolved
}
message.skipPush = skipPush
message.skipEnrichUrl = skipEnrichUrl
message.reactionScores = [:]
Expand All @@ -686,9 +695,12 @@ extension NSManagedObjectContext: MessageDatabaseSession {
message.user = currentUserDTO.user
message.channel = channelDTO

let newLastMessageAt = max(channelDTO.lastMessageAt?.bridgeDate ?? createdAt, createdAt).bridgeDate
channelDTO.lastMessageAt = newLastMessageAt
channelDTO.defaultSortingAt = newLastMessageAt
let shouldNotUpdateLastMessageAt = isSystem && channelDTO.config.skipLastMsgAtUpdateForSystemMsg
if !shouldNotUpdateLastMessageAt {
let newLastMessageAt = max(channelDTO.lastMessageAt?.bridgeDate ?? createdAt, createdAt).bridgeDate
channelDTO.lastMessageAt = newLastMessageAt
channelDTO.defaultSortingAt = newLastMessageAt
}

if let parentMessageId = parentMessageId,
let parentMessageDTO = MessageDTO.load(id: parentMessageId, context: self) {
Expand Down Expand Up @@ -1245,10 +1257,14 @@ extension MessageDTO {
.sorted { ($0.attachmentID?.index ?? 0) < ($1.attachmentID?.index ?? 0) }
.compactMap { $0.asRequestPayload() }

// At the moment, we only provide the type for system messages when creating a message.
let systemType = isSystem ? MessageType.system.rawValue : nil

return .init(
id: id,
user: user.asRequestBody(),
text: text,
type: systemType,
command: command,
args: args,
parentId: parentMessageId,
Expand Down
3 changes: 3 additions & 0 deletions Sources/StreamChat/Database/DatabaseSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ protocol MessageDatabaseSession {
mentionedUserIds: [UserId],
showReplyInChannel: Bool,
isSilent: Bool,
isSystem: Bool,
quotedMessageId: MessageId?,
createdAt: Date?,
skipPush: Bool,
Expand Down Expand Up @@ -220,6 +221,7 @@ extension MessageDatabaseSession {
pinning: MessagePinning?,
quotedMessageId: MessageId?,
isSilent: Bool = false,
isSystem: Bool,
skipPush: Bool,
skipEnrichUrl: Bool,
attachments: [AnyAttachmentPayload] = [],
Expand All @@ -239,6 +241,7 @@ extension MessageDatabaseSession {
mentionedUserIds: mentionedUserIds,
showReplyInChannel: false,
isSilent: isSilent,
isSystem: isSystem,
quotedMessageId: quotedMessageId,
createdAt: nil,
skipPush: skipPush,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
<attribute name="isHardDeleted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isShadowed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isSilent" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isSystem" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="latestReactions" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer"/>
<attribute name="locallyCreatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="localMessageStateRaw" optional="YES" attributeType="String"/>
Expand Down
37 changes: 36 additions & 1 deletion Sources/StreamChat/StateLayer/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ public class Chat {
text: text,
pinning: pinning,
isSilent: silent,
isSystem: false,
command: nil,
arguments: nil,
attachments: attachments,
Expand All @@ -471,7 +472,41 @@ public class Chat {
eventNotificationCenter.process(NewMessagePendingEvent(message: localMessage))
return try await sentMessage
}


/// Sends a system message to the channel.
///
/// - Parameters:
/// - text: Text of the message.
/// - messageId: A custom id for the sent message. By default, it is automatically generated by Stream.
/// - extraData: Additional extra data of the message object.
@discardableResult
public func sendSystemMessage(
with text: String,
messageId: MessageId? = nil,
extraData: [String: RawJSON] = [:]
) async throws -> ChatMessage {
let localMessage = try await channelUpdater.createNewMessage(
in: cid,
messageId: messageId,
text: text,
pinning: nil,
isSilent: false,
isSystem: true,
command: nil,
arguments: nil,
attachments: [],
mentionedUserIds: [],
quotedMessageId: nil,
skipPush: false,
skipEnrichUrl: false,
extraData: extraData
)
// Important to set up the waiter immediately
async let sentMessage = try await waitForAPIRequest(localMessage: localMessage)
eventNotificationCenter.process(NewMessagePendingEvent(message: localMessage))
return try await sentMessage
}

/// Edits the specified message in the channel.
///
/// - Parameters:
Expand Down
7 changes: 7 additions & 0 deletions Sources/StreamChat/Workers/ChannelUpdater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ class ChannelUpdater: Worker {
id: .newUniqueId,
user: user,
text: message,
type: nil,
command: nil,
args: nil,
parentId: nil,
Expand Down Expand Up @@ -285,6 +286,7 @@ class ChannelUpdater: Worker {
/// - text: Text of the message.
/// - pinning: Pins the new message. Nil if should not be pinned.
/// - isSilent: A flag indicating whether the message is a silent message. Silent messages are special messages that don't increase the unread messages count nor mark a channel as unread.
/// - isSystem: A flag indicating whether the message is a system message.
/// - attachments: An array of the attachments for the message.
/// - quotedMessageId: An id of the message new message quotes. (inline reply)
/// - skipPush: If true, skips sending push notification to channel members.
Expand All @@ -298,6 +300,7 @@ class ChannelUpdater: Worker {
text: String,
pinning: MessagePinning? = nil,
isSilent: Bool,
isSystem: Bool,
command: String?,
arguments: String?,
attachments: [AnyAttachmentPayload] = [],
Expand All @@ -323,6 +326,7 @@ class ChannelUpdater: Worker {
mentionedUserIds: mentionedUserIds,
showReplyInChannel: false,
isSilent: isSilent,
isSystem: isSystem,
quotedMessageId: quotedMessageId,
createdAt: nil,
skipPush: skipPush,
Expand Down Expand Up @@ -673,6 +677,7 @@ class ChannelUpdater: Worker {
id: .newUniqueId,
user: userRequestBody,
text: text,
type: nil,
extraData: [:]
)
return messagePayload
Expand Down Expand Up @@ -730,6 +735,7 @@ extension ChannelUpdater {
text: String,
pinning: MessagePinning? = nil,
isSilent: Bool,
isSystem: Bool,
command: String?,
arguments: String?,
attachments: [AnyAttachmentPayload] = [],
Expand All @@ -746,6 +752,7 @@ extension ChannelUpdater {
text: text,
pinning: pinning,
isSilent: isSilent,
isSystem: isSystem,
command: command,
arguments: arguments,
attachments: attachments,
Expand Down
1 change: 1 addition & 0 deletions Sources/StreamChat/Workers/MessageUpdater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ class MessageUpdater: Worker {
mentionedUserIds: mentionedUserIds,
showReplyInChannel: showReplyInChannel,
isSilent: isSilent,
isSystem: false,
quotedMessageId: quotedMessageId,
createdAt: nil,
skipPush: skipPush,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class DatabaseSession_Mock: DatabaseSession {
mentionedUserIds: [UserId],
showReplyInChannel: Bool,
isSilent: Bool,
isSystem: Bool,
quotedMessageId: MessageId?,
createdAt: Date?,
skipPush: Bool,
Expand All @@ -155,6 +156,7 @@ class DatabaseSession_Mock: DatabaseSession {
mentionedUserIds: mentionedUserIds,
showReplyInChannel: showReplyInChannel,
isSilent: isSilent,
isSystem: isSystem,
quotedMessageId: quotedMessageId,
createdAt: createdAt,
skipPush: skipPush,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ final class ChannelUpdater_Mock: ChannelUpdater {
@Atomic var createNewMessage_cid: ChannelId?
@Atomic var createNewMessage_text: String?
@Atomic var createNewMessage_isSilent: Bool?
@Atomic var createNewMessage_isSystem: Bool?
@Atomic var createNewMessage_skipPush: Bool?
@Atomic var createNewMessage_skipEnrichUrl: Bool?
@Atomic var createNewMessage_command: String?
Expand Down Expand Up @@ -343,6 +344,7 @@ final class ChannelUpdater_Mock: ChannelUpdater {
text: String,
pinning: MessagePinning?,
isSilent: Bool,
isSystem: Bool,
command: String?,
arguments: String?,
attachments: [AnyAttachmentPayload],
Expand All @@ -357,6 +359,7 @@ final class ChannelUpdater_Mock: ChannelUpdater {
createNewMessage_cid = cid
createNewMessage_text = text
createNewMessage_isSilent = isSilent
createNewMessage_isSystem = isSystem
createNewMessage_skipPush = skipPush
createNewMessage_skipEnrichUrl = skipEnrichUrl
createNewMessage_command = command
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ final class ChannelEndpoints_Tests: XCTestCase {
id: .unique,
user: .dummy(userId: .unique),
text: .unique,
type: nil,
command: .unique,
args: .unique,
parentId: .unique,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ final class MessageEndpoints_Tests: XCTestCase {
id: .unique,
user: .init(id: .unique, name: .unique, imageURL: .unique(), extraData: .init()),
text: .unique,
type: nil,
extraData: [:]
)

Expand Down
Loading
Loading